SSブログ

LINE APIとSlack APIの両方からのイベントをラズパイで受け付ける [Raspberry Pi]

前回の記事:LINEからラズパイ経由でGoogle Home(Nest Hub)のYouTube再生リストのキューに動画をどんどん追加する で、
Slackからのイベントも同じように受け付け、LINE BotだけでなくSlack Botでも
Google Home(Nest Hub)へYouTubeプレイリストの追加を可能にし、
共通のラズパイのwebhookサーバでLINEと同様に結果を通知するように拡張する。

システムイメージ
00.png
前提と方針は前回と同様

https://api.slack.com/appsにアクセスし、アプリを作成
01.PNG

②適当なアプリ名と、アプリを適用するワークスペースを選択
02.png

③Basic Information>Permissionsを選択
03.png

④Scopes>Bot Token Scopes>Add an OAuth Scopeを選択
04.PNG

⑤channels:historyとchat:writeを追加
05.PNG

⑥OAuth Tokens & Redirect URLs>Install App to Workspaceを選択
06.PNG

⑦ワークスペースに対してアクセス権の許可を問われるので、許可するを選択
07.png

⑧前の画面に戻り、Bot User OAuth Access Tokenにトークンが発行されているので、
値をコピーしておく
08.png

⑨下記が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に上記でコピーしたトークンを設定する
09.PNG

この時点で、webhookサーバーを立ち上げておく

⑩Event Subscriptions>Enable EventsをONにする
10.png

⑪webhookのULRの入力欄が表示されるので、上記で立ち上げたサーバのURLを入力する
※独自ドメインとラズパイのサーバの紐付けは前回記事参照
11.PNG

⑫入力して少し待つと自動でチェックが行われるので、Verifiedと表示されれば成功
※表示されない場合はSSL証明書が有効でなかったり、ドメインとの紐付けが
 失敗してるなどがあるので、各自設定できているかを確認する
12.PNG

⑬Subscribe to bot eventsのプルダウンを開き、Add Bot User Eventを選択
13.PNG

⑭message:channelsを追加
14.PNG

⑮右下のSave Changesで設定を保存
15.PNG

⑯Slackアプリの方に移り、イベント受付をさせたいチャンネルの設定を開く
16.png

⑰一番下のAppを選択
17.png

⑱右下の+を選択
18.png

⑲上記で設定したアプリを追加
19.png

⑳チャンネルに戻り、アプリが追加されたことを確認
20.png

㉑前回同様、YouTubeのURLを投稿したり、add(改行)URLを投稿することで、
YouTube再生及びプレイリストのキューに追加が可能となる
もちろん、LINEからの投稿でも同じように追加可能
21.png


ここからは補足

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に長すぎるメッセージを投稿すると、メッセージが自動的に分割されるようになっていた

nice!(0)  コメント(0) 
共通テーマ:パソコン・インターネット

nice! 0

コメント 0

コメントを書く

お名前:[必須]
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

※ブログオーナーが承認したコメントのみ表示されます。

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。