LINE APIとSlack APIの両方からのイベントをラズパイで受け付ける [Raspberry Pi]
前回の記事:LINEからラズパイ経由でGoogle Home(Nest Hub)のYouTube再生リストのキューに動画をどんどん追加する で、
Slackからのイベントも同じように受け付け、LINE BotだけでなくSlack Botでも
Google Home(Nest Hub)へYouTubeプレイリストの追加を可能にし、
共通のラズパイのwebhookサーバでLINEと同様に結果を通知するように拡張する。
システムイメージ
Slackからのイベントも同じように受け付け、LINE BotだけでなくSlack Botでも
Google Home(Nest Hub)へYouTubeプレイリストの追加を可能にし、
共通のラズパイのwebhookサーバでLINEと同様に結果を通知するように拡張する。
システムイメージ
前提と方針は前回と同様
①https://api.slack.com/appsにアクセスし、アプリを作成
②適当なアプリ名と、アプリを適用するワークスペースを選択
③Basic Information>Permissionsを選択
④Scopes>Bot Token Scopes>Add an OAuth Scopeを選択
⑤channels:historyとchat:writeを追加
⑥OAuth Tokens & Redirect URLs>Install App to Workspaceを選択
⑦ワークスペースに対してアクセス権の許可を問われるので、許可するを選択
⑧前の画面に戻り、Bot User OAuth Access Tokenにトークンが発行されているので、
値をコピーしておく
⑨下記がSlack向けに拡張したwebhookのソースコード
この時点で、webhookサーバーを立ち上げておく
⑩Event Subscriptions>Enable EventsをONにする
⑪webhookのULRの入力欄が表示されるので、上記で立ち上げたサーバのURLを入力する
※独自ドメインとラズパイのサーバの紐付けは前回記事参照
⑫入力して少し待つと自動でチェックが行われるので、Verifiedと表示されれば成功
※表示されない場合はSSL証明書が有効でなかったり、ドメインとの紐付けが
失敗してるなどがあるので、各自設定できているかを確認する
⑬Subscribe to bot eventsのプルダウンを開き、Add Bot User Eventを選択
⑭message:channelsを追加
⑮右下のSave Changesで設定を保存
⑯Slackアプリの方に移り、イベント受付をさせたいチャンネルの設定を開く
⑰一番下のAppを選択
⑱右下の+を選択
⑲上記で設定したアプリを追加
⑳チャンネルに戻り、アプリが追加されたことを確認
㉑前回同様、YouTubeのURLを投稿したり、add(改行)URLを投稿することで、
YouTube再生及びプレイリストのキューに追加が可能となる
もちろん、LINEからの投稿でも同じように追加可能
ここからは補足
LINE APIとSlack APIで異なる動作仕様について記述する。
※両方無料版が前提
①https://api.slack.com/appsにアクセスし、アプリを作成
②適当なアプリ名と、アプリを適用するワークスペースを選択
③Basic Information>Permissionsを選択
④Scopes>Bot Token Scopes>Add an OAuth Scopeを選択
⑤channels:historyとchat:writeを追加
⑥OAuth Tokens & Redirect URLs>Install App to Workspaceを選択
⑦ワークスペースに対してアクセス権の許可を問われるので、許可するを選択
⑧前の画面に戻り、Bot User OAuth Access Tokenにトークンが発行されているので、
値をコピーしておく
⑨下記がSlack向けに拡張したwebhookのソースコード
import ssl
import subprocess
import requests
import json
import re
import urllib.parse
from flask import Flask, request, jsonify, make_response
from casttube import YouTubeSession
fullchain_path = 'fullchain.pemのフルパスを記載'
prvkey_path = 'privkey.pemのフルパスを記載'
server_port = 設定したポート番号を記載
line_token = 'LINEのトークンを記載'
slack_token = 'Slackのトークンを記載'
device_name = 'Google Home(Nest Hub)のデバイス名を記載'
device_screen_id = 'Google Home(Nest Hub)のスクリーンIDを記載'
line_reply_api = 'https://api.line.me/v2/bot/message/reply'
slack_push_api = 'https://slack.com/api/chat.postMessage'
youtube_long_url = 'https://www.youtube.com/watch?'
youtube_short_url = 'https://youtu.be/'
app = Flask(__name__)
session = YouTubeSession(device_screen_id)
@app.route('/', methods=['GET'])
def get_reply():
return jsonify({}), 200
@app.route('/', methods=['POST'])
def post_reply():
req_from = 'line' if 'events' in request.json else 'slack' if 'event' in request.json else ''
reply_to = ''
req_message = ''
res_headers = {'Content-Type': 'application/json'}
res_body = {}
reply_message = ''
ignore = True
if 'line' == req_from:
req_json = request.json['events'][0]
reply_to = req_json['replyToken']
req_message = req_json['message']['text']
ignore = False
elif 'slack' == req_from:
res_headers.update({'X-Slack-No-Retry':1})
if 'challenge' in request.json:
res_body = {'challenge':request.json['challenge']}
else:
req_json = request.json['event']
if 'X-Slack-Retry-Num' in request.headers and 'X-Slack-Retry-Reason' in request.headers:
print('ignore slack retry')
elif ('slack' == req_from and
('subtype' in req_json and 'bot_message' == req_json['subtype'] or 'bot_profile' in req_json)):
print('ignore slack bot')
elif 'text' in req_json:
reply_to = req_json['channel']
req_message = req_json['text']
ignore = False
if not ignore:
try:
messages = req_message.split('\n')
if 'add' == messages[0] or 'next' == messages[0] or 'remove' == messages[0]:
reply_message = messages[0] + '\n'
lines = messages[1:]
if 'next' == messages[0]:
lines = reversed(lines)
for line in lines:
url = modify_url(line)
id = extract_id(url)
if '' == id:
reply_message += 'invalid url:'
else:
if 'add' == messages[0]:
session.add_to_queue(id)
elif 'next' == messages[0]:
session.play_next(id)
else:
session.remove_video(id)
reply_message += url + '\n'
reply_message = reply_message.rstrip('\n')
elif 'clear' == messages[0]:
session.clear_playlist()
reply_message = 'clear'
else:
url = modify_url(messages[0])
id = extract_id(url)
if '' != id:
cast_cmd = ['catt', '-d', device_name, 'cast', url]
try:
subprocess.check_output(cast_cmd, stderr=subprocess.STDOUT).decode()
reply_message = 'cast\n' + url
except subprocess.CalledProcessError as e:
reply_message = 'cast error\n' + url + '\n' + e.output.decode()
except Exception as e:
reply_message = str(e.args)
if '' != reply_message:
result = reply_line_text(reply_to, reply_message) if 'line' == req_from \
else push_slack_text(reply_to, reply_message)
if 200 != result.status_code:
print('reply failed:' + str(result.status_code))
return make_response(json.dumps(res_body), 200, res_headers)
def modify_url(url):
return url.lstrip('<').rstrip('>') if url.startswith('<') and url.endswith('>') \
and re.search('^https?://[\w/:%#$&\?\(\)~\.=\+\-]+', url.lstrip('<').rstrip('>')) \
else url
def extract_id(url):
return urllib.parse.parse_qs(urllib.parse.urlparse(url).query)['v'][0] if re.match(youtube_long_url, url) \
else url[len(youtube_short_url):len(youtube_short_url) + 11] if re.match(youtube_short_url, url) \
else ''
def make_res_headers(token):
return {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + token
}
def reply_line_text(reply_token, text):
return requests.post(
line_reply_api,
data=json.dumps({
'replyToken': reply_token,
'messages': [
{
'type': 'text',
'text': text
}
]
}).encode('utf-8'),
headers=make_res_headers(line_token)
)
def push_slack_text(channel, text):
return requests.post(
slack_push_api,
data=json.dumps({
'channel':channel,
'text':text
}).encode('utf-8'),
headers=make_res_headers(slack_token)
)
if __name__ == '__main__':
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_cert_chain(fullchain_path, prvkey_path)
print('start:' + str(server_port))
app.run(host='0.0.0.0', port=server_port, ssl_context=context, threaded=True)
slack_tokenに上記でコピーしたトークンを設定するこの時点で、webhookサーバーを立ち上げておく
⑩Event Subscriptions>Enable EventsをONにする
⑪webhookのULRの入力欄が表示されるので、上記で立ち上げたサーバのURLを入力する
※独自ドメインとラズパイのサーバの紐付けは前回記事参照
⑫入力して少し待つと自動でチェックが行われるので、Verifiedと表示されれば成功
※表示されない場合はSSL証明書が有効でなかったり、ドメインとの紐付けが
失敗してるなどがあるので、各自設定できているかを確認する
⑬Subscribe to bot eventsのプルダウンを開き、Add Bot User Eventを選択
⑭message:channelsを追加
⑮右下のSave Changesで設定を保存
⑯Slackアプリの方に移り、イベント受付をさせたいチャンネルの設定を開く
⑰一番下のAppを選択
⑱右下の+を選択
⑲上記で設定したアプリを追加
⑳チャンネルに戻り、アプリが追加されたことを確認
㉑前回同様、YouTubeのURLを投稿したり、add(改行)URLを投稿することで、
YouTube再生及びプレイリストのキューに追加が可能となる
もちろん、LINEからの投稿でも同じように追加可能
ここからは補足
LINE APIとSlack APIで異なる動作仕様について記述する。
※両方無料版が前提
動作仕様 | LINE | Slack |
webhookのチェック | 200を返せばJSONの中身は不問 | 返すJSONにリクエストのchallengeキーの値を返す必要がある |
リトライ | なし | 3秒以内に返さないとリトライ 2xx以外を返したりエラーなどが返ってくると、一定期間後にリトライ |
レスポンス速度 | 速い | 若干遅い AWSを利用しているようで、メッセージの投稿からwebhookサーバに届くまで1秒位時間がかかっている |
回数制限 | リプライの場合はおそらくなし ※Push APIを利用する場合は短期連続使用で時間制限、また月制限がある |
1秒に付き1回 |
文字数制限 | 2,000文字 | 約4,000文字 ※超える場合は分割されて別フィールドに設定される |
メッセージ内URLの形式 | そのままの文字データ | <>で囲まれる |
Bot自身のイベント | 通知されない | 通知される |
webhookのチェックとリトライとURL形式、Bot自身のイベントに関しては、
Slack特有の仕様があるので、上記のwebhookサーバのソースコードは
それぞれに対応するように拡張している。
リトライに関しては、ヘッダにX-Slack-Retry-Numキーがあればリトライありとなる。
値にはリトライの回数が設定されており、またX-Slack-Retry-Reasonキーには
リトライの理由が記述される。
上記コードでは、理由問わずX-Slack-Retry-Numがあれば無視するようにしている。
また、レスポンスのヘッダに'X-Slack-No-Retry':1を設定することで、
2xx以外で返すときなどにリトライをしないように設定している。
また、Bot自身のイベントについては、bot_profileキーがBodyにあればBotからの投稿である。
リトライ同様こちらもキーがある時点で無視するようにしている
(nameキーにBot名があるので、それで判定して当該Botだけ無視するようにすることも可能)。
※subtypeキーも見て無視するようにしているが、これは本記事では紹介していない
Incoming Webhooksで投稿されたメッセージになるので、これもまた
使用する場合を考慮し無視するようにしている
ちなみに、このリトライやBot無視の対応しないと無限ループが発生し、永久に
botが通知し続けるので注意
URLは<>で囲まれるので、除去後がURLの形式になっていれば<>をメッセージから除去する
参考
SlackからPythonサーバーにメッセージを送信する
Slack Events APIの再送仕様と回避方法まとめ(Serverless on AWS)
LINE Messaging API でできることまとめ【送信編】
slackに長すぎるメッセージを投稿すると、メッセージが自動的に分割されるようになっていた
コメント 0