【PowerAutomate】Yammerのコミュニティで使用しているトピック一覧を作る【暫定版】

# これを作ったきっかけ
Yammerでコミュニティを運営しているけどトピック機能に気づいてもらえない
　↓
過去メッセージが探しづらいとの要望（トピックを使って欲しい）
　↓
トピックの一覧を公開すれば気づいてもらえるのでは？

ということで試行錯誤した結果です。

# Yammer標準でそんな機能あるのでは…
Microsoftコミュニティの[こちら](https://answers.microsoft.com/en-us/msoffice/forum/all/how-can-i-get-the-list-of-topic-in-yammer/b33c4103-1f21-4591-a0d3-0a74cf597e39)を見る限り、以前は「www.yammer.com/(domainname)/topics」で取得できていたようですが、2022年10月現在の私のテナントでは機能しませんでした。

# それでもPowerAutomateなら…
Yammerコネクタを見る限り該当しそうなアクションはありません。
しかし「グループ内のメッセージを取得する (V2)」アクションの出力にある body/references 下に下表のような情報が含まれています。 

※2022年10月現在の Yammer & PowerAutomate ではありました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884114/451aee9e-75bf-7652-f805-12594ffcadea.png)

| type | 中身 |
|:---|:---|
| topic | トピックの定義|
| tag | ハッシュタグの定義 |
| thread | スレッド(トピックとの紐付け含む) |
| group | コミュニティの定義 |
| user | ユーザーの定義 |

* "type":"topic"
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884114/ef33d6b0-00d0-bc19-ed96-1b739a6d03bf.png)

* "type":"tag"
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884114/c27849a3-7c3f-77d0-57f5-ca5363ae4b2c.png)


* "type":"thread"
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884114/f41590dc-1cbb-d543-bb72-23b5f93847ad.png)

ここに含まれている情報は、あくまで取得したメッセージが参照しているトピック/ハッシュタグ/スレッドのみのようです。その為、全てのメッセージを取得すればトピックも全て収集できそうです。

## 備忘録
"type":"tag"のハッシュタグの情報は削除されたものも含まれている模様。
アクティブなハッシュタグのみを識別することができるか？

# フローの構成
今回は取得したデータをSharePoint Listsに出力します。

1. トピック情報の取得
2. Lists 既存アイテムの取得
3. 新規トピックの抽出⇨リストに登録
4. 更新されたトピックの抽出⇨リストに登録

※未使用となったリストアイテムの削除は未実装

## フロー全体図
こんな感じになりました。
<img alt="flow-all-closed_1a.png" width=600 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884114/6d9ba7f6-6795-1a0c-ea20-18ce4ce55446.png">


## 0.トリガー・変数を定義
<img alt="flow-0.png" width=600 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884114/c1dbd51b-2d77-d789-ea85-e0160719f0fc.png">

トリガーは仮の設定です。お好みで。
変数は3つ定義します。

|変数名|種類|初期値|用途|
|:---|:---|:---|:---|
|メッセージID|整数|なし|「グループ内のメッセージを取得する (V2)」アクションの「削除までの期間」にセットするメッセージIDを格納|
|ItemCount|整数|0|Do Untilループの条件指定用|
|TopicArray|アレイ|なし|Yammerから取得したトピックの情報を格納する変数|


## 1.トピック情報の取得（この記事の主題）

### 1-1.「グループ内のメッセージを取得する (V2)」& 「Do Until ループ」ですべてのメッセージを取得する
Do Until でループさせコミュニティのメッセージを取得する処理はこんな形にしました。
全てのメッセージを取得することで、メッセージに紐づいたトピックの情報を body/references下から取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884114/c6826530-6ee5-57c9-e171-acfcdb6df26c.png)


#### ポイント
* 「グループ内のメッセージを取得する (V2)」アクションは「最大」という設定項目がありますが何を指定しても返ってくる件数は20件までで実質機能しません。
* 「改ページ処理」をONにすれば20件以上返ってきますが、その際にはbody/referenes の値が返ってこなくなる為、今回の目的には適しません。

#### ではどうする？
「グループ内のメッセージを取得する (V2)」アクションには「次より古い」というパラメータがあります。このパラメータに取得したメッセージ20件の末尾のメッセージIDを渡してループで回すことで、どんどん遡ってメッセージを取得していくことができ、それに伴いbody/references下の"type":"topic"もメッセージに対応した情報を得ることができます。

#### Yammerコネクタのドキュメントに答えがあった
フローを完成させてから気付いたのですが、[Yammerコネクタドキュメントの制限事項](https://learn.microsoft.com/ja-jp/connectors/yammer/#known-issues-and-limitations)に「！注意」として全てのメッセージを取得する方法の記載がありました。ドキュメントはよく読んだ方が良いですね。↑の説明よりも簡潔な説明がされています。
> 注意
>Yammer API は、メッセージの数を 20 に制限します。 この制限を回避するには、[Microsoft Flow を使って Yammer グループの全てのメッセージを取得する方法](https://alextofan.com/2019/03/18/how-to-get-all-messages-in-a-yammer-group-using-microsoft-flow/)を参照してください。 Yammer APIの制限についての詳細は、Yammer API レート制限にアクセスしてください。

### JSONの解析
body/references下の解析に、スキーマは下記を使っています。
~~~{
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "type": {
                "type": "string"
            },
            "id": {
                "type": "integer"
            },
            "network_id": {
                "type": "integer"
            },
            "name": {
                "type": "string"
            },
            "url": {
                "type": "string"
            },
            "web_url": {
                "type": "string"
            },
            "state": {
                "type": "string"
            },
            "full_name": {
                "type": "string"
            },
            "job_title": {
                "type": [
                    "string",
                    "null"
                ]
            },
            "mugshot_url": {
                "type": "string"
            },
            "mugshot_redirect_url": {
                "type": "string"
            },
            "mugshot_url_template": {
                "type": "string"
            },
            "mugshot_redirect_url_template": {
                "type": "string"
            },
            "activated_at": {
                "type": "string"
            },
            "auto_activated": {
                "type": "boolean"
            },
            "stats": {
                "type": "object",
                "properties": {
                    "following": {
                        "type": "integer"
                    },
                    "followers": {
                        "type": "integer"
                    },
                    "updates": {
                        "type": "integer"
                    }
                }
            },
            "email": {
                "type": "string"
            },
            "aad_guest": {
                "type": "boolean"
            },
            "thread_starter_id": {
                "type": "integer"
            },
            "group_id": {
                "type": [
                    "integer",
                    "null"
                ]
            },
            "topics": {
                "type": "array"
            },
            "privacy": {
                "type": "string"
            },
            "announcement": {
                "type": "boolean"
            },
            "direct_message": {
                "type": "boolean"
            },
            "has_attachments": {
                "type": "boolean"
            },
            "reply_disabled": {
                "type": "boolean"
            },
            "invited_user_ids": {
                "type": "array"
            },
            "read_only": {
                "type": "boolean"
            },
            "description": {
                "type": "string"
            },
            "mugshot_id": {
                "type": "string"
            },
            "show_in_directory": {
                "type": "string"
            },
            "created_at": {
                "type": "string"
            },
            "members": {
                "type": "integer"
            },
            "aad_guests": {
                "type": "integer"
            },
            "color": {
                "type": "string"
            },
            "external": {
                "type": "boolean"
            },
            "moderated": {
                "type": "boolean"
            },
            "header_image_url": {
                "type": "string"
            },
            "category": {
                "type": "string"
            },
            "default_thread_starter_type": {
                "type": "string"
            },
            "restricted_posting": {
                "type": "boolean"
            },
            "company_group": {
                "type": "boolean"
            },
            "id_str": {
                "type": "string"
            },
            "creator_id": {
                "type": "integer"
            },
            "normalized_name": {
                "type": "string"
            },
            "permalink": {
                "type": "string"
            },
            "followers_count": {
                "type": "integer"
            },
            "sender_id": {
                "type": "integer"
            },
            "delegate_id": {},
            "replied_to_id": {
                "type": [
                    "integer",
                    "null"
                ]
            },
            "sender_type": {
                "type": "string"
            },
            "thread_id": {
                "type": "integer"
            },
            "message_type": {
                "type": "string"
            },
            "system_message": {
                "type": "boolean"
            },
            "content_excerpt": {
                "type": "string"
            },
            "language": {
                "type": "string"
            },
            "body": {
                "type": "object",
                "properties": {
                    "plain": {
                        "type": "string"
                    },
                    "parsed": {
                        "type": "string"
                    },
                    "rich": {
                        "type": "string"
                    }
                }
            },
            "liked_by": {
                "type": "integer"
            }
        },
        "required": [
            "type",
            "id",
            "network_id",
            "url",
            "web_url"
        ]
    }
}
~~~

### 1-2.「グループ内のメッセージを取得する (V2)」からトピック情報を取得する
上の図の「スコープ-topicを抽出して変数に追加」の箇所はこのようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884114/f0e193a8-78cf-3e2e-a45e-5534b23413c1.png)
#### ポイント
* Do Untilループ内での処理は最小に留めたかったので、取得したトピックをTopicArrayに格納していきます。
「選択」で作成したアレイの要素をunion関数でTopicArrayに追加しています。union関数を使う事で重複排除しつつ変数に設定でき一石二鳥な感じです。
(参考にしたページ： https://mofumofupower.hatenablog.com/entry/union_multi_array)
* 「web_url」として返されるURLはブラウザからはアクセスできませんでした。そこでYammerでトピックを検索した結果ページのURLに置き換えています。
~~~
・"web_url"の値 →Webブラウザからアクセスできない
 https://www.yammer.com/[tenant].onmicrosoft.com/topics/[topic id]

・代わりにURLとしてセットする値 = Yammerでトピックを検索した結果ページのURL
https://web.yammer.com/main/org/[tenant].onmicrosoft.com/search/topics?search=[TopicName(urlエンコード)]
~~~

## 2.Lists 既存アイテムの取得
あとはTopicArray変数に格納したトピックをListsに出力するだけです。

### 2-1.Listsの定義
格納するリストは下表のような項目を用意しています。
|列名|種類|メモ|
|:---|:---|:---|
|Title|1 行テキスト|TopicID|
|TopicName|1 行テキスト|トピックの表示名|
|URL|ハイパーリンクまたは画像||
|概要|1 行テキスト||
|スレッド数|数値|未使用(今後の改良予定)|

### 2-2.フロー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884114/2db92dc2-4c51-7530-0d3f-ffe822df80f7.png)
Yammerから取得したトピック情報は変数に入っているので、リストアイテムも変数に読み込んで、変数同士を比較して新規アイテム・更新アイテムを判別し必要なものだけリストに戻すことで、ループ処理を極力少なくしています。
2.は以降の処理の為の準備です。

リストの全アイテムを読み込み、TopicID、トピック名、概要欄の3つのアレイを作っています。

※この辺りベストプラクティスを見つけられず試行錯誤中です。

## 3. 新規トピックの抽出⇨リストに登録
TopicArray変数(Yammerから取得したトピックのアレイ)と、既存リストアイテムのトピックIDのアレイを比較し、存在しないもの＝新規アイテムだけを抽出し、リストに登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884114/6b3fbbb9-7250-3247-2e40-ace3c8ac7b79.png)

(参考にしたページ： https://mofumofupower.hatenablog.com/entry/2020/04/25/233437)


## 4. 更新されたトピックの抽出⇨リストに登録
「TopicIDが一致してTopicNameが異なるアイテム」と「TopicIDが一致してdescriptionが異なるアイテム」を抽出してリストを更新します。

(参考にしたページ： https://mofumofupower.hatenablog.com/entry/2020/10/09/142351)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884114/81db136b-287a-a506-34eb-8787da600270.png)

# 実行結果イメージ
リストにコミュニティ内で使用されたトピックの情報が出力されます。
これをタイル表示風のビューを作ってSharePointサイトで公開しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884114/a336e4cd-e61d-1fda-2619-fbe813328758.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884114/c3865df1-1d4d-05ae-ee96-553bafaca3cd.png)

# 「暫定版」としている理由・懸念点
* パブリックコミュニティでのみ動作確認しています。プライベートコミュニティでも同じはず（未検証）
* Do Untilループを使っていますがこれを使うとととにかく速度が出ません。投稿メッセージ数1000件程度のコミュニティでは問題ありませんでしたが、データ量が増えた時にどうなるか未確認です。
* [Yammerコネクタドキュメントの制限事項](https://learn.microsoft.com/ja-jp/connectors/yammer/#known-issues-and-limitations)が↓に影響しそう
* 「Yammer REST API の制限により、次のアクションとトリガーはすべてのメッセージを返すことを保証しません。」と記述がありますので…トピック情報にも欠落がある可能性があります（推測）
* Yammer REST APIの制限により、「All Company」コミュニティではこの手法は使えないようです。

# (余談)コミュニティ内ではなくYammerのすべてのトピックを取得できるか？
「すべてのメッセージを取得する (V2)」アクションの出力にも body/references はあるのでトピックの情報を収集することはできます。しかし、下記のような仕様がありすべて取得するのは難しそうです。(環境が整っておらず未検証です)
* 「すべてのメッセージを取得する (V2)」はパブリックコミュニティのメッセージのみ取得する仕様
* 「すべてのメッセージを取得する (V2)」アクションはすべてのメッセージを返すことを保証しない

# 今後のフロー改良予定（未定）
* "type":"thread" からトピックの参照数を集計し、リストでよく使われてる順表示を実現したい


# 最後に
本フローの作成には、[Microsoft MVP Hiroさん](https://qiita.com/h-nagao)のブログ「[MoreBeerMorePower](https://mofumofupower.hatenablog.com/)」を大いに参考にさせていただきました。ありがとうございます！