DiscordのアクティビティをSlackのStatusに反映する話

# 概要
Discordのアクティビティ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563800/b6f7df35-7f1b-3b4c-4445-a34882e78260.png)
を
SlackのStatusに反映した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563800/a914e204-4b5c-dcbe-ed06-86a6874d6c72.png)

誰得なんだこれは…。


# discord.py
今回はDiscord APIのPythonライブラリである**discord.py**を利用する。

- [公式リファレンス](https://discordpy.readthedocs.io/ja/latest/index.html)

## 参考
Tokenの取得などは偉大なる先駆者の方々が既に記事を書いているのでそちらを参照してもらいたい。

- [Pythonで実用Discord Bot(discordpy解説)](https://qiita.com/1ntegrale9/items/9d570ef8175cf178468f)

# 実装
基本的には自分で建てたdiscordサーバにbotを置いて、そのbotがサーバメンバーのアクティビティを取得するような形になる。
※もっと良い方法があれば教えて欲しい。

discordの各イベントハンドラに処理を記述していくことになるが、
今回は

- bot起動時
- メンバーのプロフィール更新時

にアクティビティを取得することとする。

それぞれのイベントハンドラは以下の通り。

```python
# bot起動時
@client.event
async def on_ready():
     ...
```

```python
# メンバーのプロフィール更新時
@client.event
async def on_member_update(before, after):
     ...
```

## アクティビティを取得する
公式リファレンスを読む限り、[discord.Member](https://discordpy.readthedocs.io/ja/latest/api.html#member
)にユーザのアクティビティを持っているとのこと。

つまり各イベントハンドラで`Member`を取得できればよい。
そこで、`Client.get_all_members()`を使うことにした。

ただしサーバに参加しているすべてのメンバーを取得してしまうため、
`discord.util`関数を使って指定したメンバーの`Member`のみ取得する。

```python
member = discord.utils.get(client.get_all_members(), name='Discord上の名前')
if member.activity != None:
    print(member.activity.name)
```
KovaaKを起動していた場合、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563800/b6f7df35-7f1b-3b4c-4445-a34882e78260.png)

```:実行結果
KovaaK 2.0: The Meta
```
となる。

### アクティビティを取得する上での注意点
`discord.Member.activity`は様々なアクティビティを持っており、
ゲームの場合は上記で上手くいくが、Discordに登録されていないゲームやSpotify再生中などのアクティビティはもしかしたらうまく行かないかもしれない。

詳しくは下記を参照。

- [discord.Member.activity](https://discordpy.readthedocs.io/ja/latest/api.html#discord.Member.activity)
- [discord.BaseActivity](https://discordpy.readthedocs.io/ja/latest/api.html#discord.BaseActivity)

一応`BaseActivity`のメンバと`Spotify`はみんな`name`を持ってるから大丈夫か…な…（多分）

## Slackのステータスを書き換える
Slack APIの`users.profile.set`にpostリクエストを投げればできる。

- [users.profile.set](https://api.slack.com/methods/users.profile.set)
　→　Testerタブでいろんなリクエストをテストできるぞ！

基本的には`name`に更新したいプロファイル名、`value`にプロファイルの内容を渡すことによって実現可能。

```python
response = requests.post('https://slack.com/api/users.profile.set', 
                       data={'token': 'Slackのトークン',
                       'name': 'status_text',
                       'value': 'test',
                       'user': 'SlackのユーザID', 
                       'pretty': '1'})
```
これを投げると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563800/f6c61547-19df-fcd2-58ec-f17cfacdbf4d.png)
こんな感じになる。

## これらをまとめるとこんな感じ
※Python初心者のコードなので見苦しい点あればすみません……。

```python
import discord
import requests

DISCORD_TOKEN = '******'
SLACK_TOKEN = '******'

game = None

# Discordに接続するためのオブジェクトを生成
client = discord.Client()

# memberからアクティビティの名前を取得して変数にセット
def set_game_name(member):
    global game
    if member.activity != None:
        game = member.activity.name + 'をプレイ中'
    else:
        game = 'Not Playing game.'

# Slackのstatusを更新
def post_slack_status():
    global game
    response = request.post('https://slack.com/api/users.profile.set',
                            data={
                                 'token': SLACK_TOKEN,
                                 'profile': '{ "status_text":"' + game + '",
                                               "status_emoji": ":nick:" }',
                                 'user': '*****',
                                 'pretty': '1'})

# bot起動時
@client.event
async def on_ready():
    member = discord.utils.get(client.get_all_members(), name='*****')
    set_game_name(member)
    post_slack_status()

# メンバーのプロフィール更新時
@client.event
async def on_member_update(befor, after):
    set_game_name(after)
    post_slack_status()

# bot起動
client.run(DISCORD_TOKEN)
```

ね？簡単でしょ？

## 友人A「n分経過も出せるといい」
詳しい実装方法は後日気が向けば記載します…。
簡単に説明すると、

- `post_slack_status()`のリクエストを、1分ごとに回るループ内で投げる。
- `post_slack_status()`をスレッド化して別スレッドとして実行。
- `on_member_update(befor, after)`が走ったら、ループのフラグを`False`にしてスレッドを終了させアクティビティを再取得後、もう一回`post_slack_status()`のスレッドを走らせる。

こういう感じ。
(書いてて思ったけど、`on_member_update()`内でスレッドの終了を待ってるときに、もう一回`on_member_update()`が走ったらどうなるんだろう……。イベントハンドラやっかいだ………。)

# 追記(2020/05/07)
`on_member_update()`の仕様理解不足があった。
[on_member_update()](https://discordpy.readthedocs.io/ja/latest/api.html#discord.on_member_update)

メンバーの以下が更新されたときに走るため、
activityが更新されたときのみ走るようにしよう！

- ステータス
- activity
- ニックネーム
- 役職