DiscordとLINEをPython+flask+Dockerで連携してみた。その0 利用したいだけの場合
作成日時 2022-09-08
最終更新 2022-11-01
タグ
#Python

こんにちは。いつもBot作ってるマグロです。
前回のDicordとLINEのメッセージ連携ですが、1から作るのはハードルが高いなあという人がいると思います。
そこですでに構築済みのものを配布しているのでそれの使い方を説明します。

ただやることがめちゃくちゃ多いので、根気と知識が必要です。

必要なもの
・GitHubのアカウント
・railwayのアカウント
・git
・プログラムをいじれるエディタ(VSCode推奨)
・GayzoのアカウントとAPIキー
・YouTubeアカウントとAPIキー

GyazoとYouTubeに関しては参考リンクを挙げておくので、挙動の確認をお願いします。
Gyazo
https://eng.shibuya24.info/entry/python_gyazo_image_upload
YouTube
https://qiita.com/ny7760/items/5a728fd9e7b40588237c
YouTubeの方は適当な動画をアップロードしてテストしてください。
GCPに申請する際、アプリケーションの種類はデスクトップにしておきましょう。
2つのjsonファイルが生成されるので控えておきましょう。

仕様とか

※一部改良前の画面を表示してます。

メッセージ、画像をこんな感じで送受信できます。

メンション、送信先チャンネルも指定可能です。
(現在、メンションは「@ユーザー名#member」,「@ロール名#role」で指定します。)


LINE側の動画をYouTubeにアップロードさせて、疑似的に動画を共有させてます。
プログラム側で標準で限定公開(URLを知ってる人しか見れない)に設定してあるので、プライバシーはあまり気にする必要はないです。
また、LINEスタンプも送れます。

また、今回使用するテンプレートはこんな感じです。

稼働しているDiscordBotは2つのサーバーに所属しており、それぞれのサーバーごとにLINEのトークルームを作成します。
この場合LINEBotを二つ用意します。
3つのサーバーに所属させる場合はLINEBotも3つ用意します。
また、flask側のプログラムに追記する必要があります。(後述)

テンプレ

https://github.com/maguro-alternative/discordbot_taityo にアクセス。
下にあるDeploy on Railwayをクリック

こんな感じの画面に遷移するのでDeployをクリック。
(Private repositoryにするかはご自由に。一応推奨してます。)


すると早速ビルドが始まります。

しかしこれだけではまだ動きません。まだ環境変数を設定していないので設定します。

環境変数

Deploymentsの隣のVariablesから環境変数を設定します。

右端の設定ボタンから編集できますが、右上にあるRAW Editorで編集しましょう。

こんな感じで直感的に編集できます。
さて、環境変数を設定していきましょう。.env.sampleから以下の通りに設定します。

SERVER_NAME=FIVE_SECOND,FIVE_HOUR
FIVE_SECOND_WEBHOOK=
FIVE_SECOND_ACCESS_TOKEN=
FIVE_SECOND_CHANNEL_SECRET=
FIVE_SECOND_GROUP_ID=
FIVE_SECOND_GUILD_ID=
FIVE_SECOND_TEMPLE_ID=
FIVE_SECOND_NG_CHANNEL=
FIVE_HOUR_WEBHOOK=
FIVE_HOUR_ACCESS_TOKEN=
FIVE_HOUR_CHANNEL_SECRET=
FIVE_HOUR_GUILD_ID=
FIVE_HOUR_TEMPLE_ID=
FIVE_HOUR_NG_CHANNEL=
PORT=8080
GYAZO_TOKEN=
TOKEN=
USER_LIMIT=100
CLIENT_SECRET_NAME=
access_token=
client_id=
client_secret=
refresh_token=
project_id=
token_expiry=
VOICEVOX_KEY=


めっちゃ多いです
一応役割について説明します。

SERVER_NAME=FIVE_SECOND,FIVE_HOUR

Discordのサーバーを識別するための環境変数です。
カンマで区切ることで、複数のサーバーを識別できるようにしてます。
上記を例とすると、
・FIVESECOND,FIVEHOURがカンマ区切りなので、運用されるDiscordサーバーは二つ。
・その後の環境変数は以下のように命名する。

FIVE_SECOND_WEBHOOK=
FIVE_SECOND_ACCESS_TOKEN=
FIVE_SECOND_CHANNEL_SECRET=
FIVE_SECOND_GROUP_ID=
FIVE_SECOND_GUILD_ID=
FIVE_SECOND_TEMPLE_ID=
FIVE_HOUR_WEBHOOK=
FIVE_HOUR_ACCESS_TOKEN=
FIVE_HOUR_CHANNEL_SECRET=
FIVE_HOUR_GUILD_ID=
FIVE_HOUR_TEMPLE_ID=

となります。
被らなければa,bとか何でもいいです。
その場合、

a_WEBHOOK=
b_WEBHOOK=

と命名しましょう。
また、一つだけ指定する場合はカンマ区切りは不要です。

ちょっとわかりづらいと思うのでプログラム例です。
プログラム内ではこういう風に指定してます。

servers_name=os.environ['SERVER_NAME']
server_list=servers_name.split(",")
for server_name in server_list:
    os.environ[f"{server_name}_GUILD_ID"]


流れ
1:server_nameにFIVE_SECOND,FIVE_HOURを代入。
2:server_listにカンマ区切りでFIVE_SECONDとFIVE_HOURをそれぞれ配列として代入。
3:forで回す。{server_name}_GUILD_IDはそれぞれ「FIVE_SECOND_GUILD_ID」,「FIVE_HOUR_GUILD_ID」となります。

こんな感じで環境変数はプログラムが勝手に割り当ててくれます。
ではそれぞれの役割も解説します。

_WEBHOOK

Discord側のWebhookです。後述する時報と警告機能を投稿します。

_ACCESS_TOKEN=

LINEBot側のアクセストークンです。例では2つのサーバーで運用しているので、その場合はLINEBotも2つ用意しましょう。

_CHANNEL_SECRET=

LINEBot側のチャネルシークレットキーです。line-bot-sdkでは必須です。

_GROUP_ID=

LINEのグループIDです。LINEBotがグループに所属していて、そこで発言させたい場合は必須です。設定しない場合は友達登録している人全員にメッセージが送信されます。
ちなみに例のFIVE_SECONDでは設定されていますが、FIVE_HOURでは設定してません。

_GUILD_ID=

Discord側のサーバーIDです。こちらもサーバーの識別に使用します。
それぞれ対応させるサーバーのIDを入れましょう。

_TEMPLE_ID=

DiscordのチャンネルIDです。基本ここで設定したチャンネルへLINEからDiscordへ送信されます。

_NG_CHANNEL=

LINE側に送りたくないDiscordのメッセージチャンネルを設定します。
こちらもSERVER_NAMEと同様にカンマ区切りで複数のチャンネルを指定できます。

FIVE_SECOND_NG_CHANNEL=ログ,管理人用

こうすることでFIVE_SECOND側のチャンネル名が「ログ」、「管理人用」の二つのチャンネルのメッセージがLINEに送られなくなります。
特に定めない場合は存在しないチャンネル名を書き込んでください。
こちらも一つだけ指定する場合はカンマ区切りは不要です。

はい!これでLINEBot周りの設定は完了です!
大体これで半分くらいです。
次は他のAPI周りの環境変数になります。

PORT=8080
USER_LIMIT=100

PORTはflaskでサーバーを立ち上げるためのポート番号です。
railway側でflaskを使う場合は必ず宣言する必要があるみたいです。(公式で言及してた)
USER_LIMITはDiscordAPIでリクエストを行う際、取得するユーザーの上限を指定してます。
値はなんでもよさそうですが、大きいと処理に時間がかかるかもしれません。

TOKEN=

DiscordBotのトークンです。やっと出てきた感じですね(笑)

GYAZO_TOKEN=

GyazoAPIのトークンです。
LINE側から画像が送信された場合、アップロードしてURLに変換します。

CLIENT_SECRET_NAME=
access_token=
client_id=
client_secret=
refresh_token=
project_id=
token_expiry=

YouTube Data API関連のものです。
事前に生成した2つのjsonファイルから参照します。

client_secret{ランダム生成された文字列}.json
upload_video.py-oauth2.json


CLIENT_SECRET_NAME

「client_secret{ランダム生成された文字列}」(.jsonまでは含まない)が入ります。
要するにjsonファイルの名前です。中身はこうなってると思います。

{
  "installed":
  	        {
  	            "client_id":os.environ["client_id"],
  	            "project_id":os.environ["project_id"],
  	            "auth_uri":"https://accounts.google.com/o/oauth2/auth",
  	            "token_uri":"https://oauth2.googleapis.com/token",
  	            "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs",
  				"client_secret":os.environ["client_secret"],
  				"redirect_uris":["http://localhost"]
  			}
  }

cilent_id,project_id,client_secretそれぞれ環境変数に該当するものになります。割り当てていきましょう。

oau={
	    "access_token":os.environ["access_token"],
	    "client_id":os.environ["client_id"],
	    "client_secret":os.environ["client_secret"],
	    "refresh_token":os.environ["refresh_token"],
	    "token_expiry": os.environ["token_expiry"], 
	    "token_uri": "https://oauth2.googleapis.com/token",
	    "user_agent": None,
	    "revoke_uri": "https://oauth2.googleapis.com/revoke", 
	    "id_token": None, 
	    "id_token_jwt": None, 
	    "token_response": {
	        "access_token":os.environ["access_token"],
	        "expires_in": 3599, 
	        "scope": "https://www.googleapis.com/auth/youtube.upload", 
	        "token_type": "Bearer"
	    },
	    "scopes": ["https://www.googleapis.com/auth/youtube.upload"], 
	    "token_info_uri": "https://oauth2.googleapis.com/tokeninfo", 
	    "invalid": False, 
	    "_class": "OAuth2Credentials", 
	    "_module": "oauth2client.client"
	}

upload_video.py-oauyh2.jsonの中身です。
access_token,refresh_token,token_expiryの該当する部分にこちらも割り当てます。

これでDiscordBot側の最低限の動作ができるようになります。

VOICEVOX_KEY=


ずんだもんで有名なvoicevoxのAPIキーです。
必須ではありませんが、割り当てるとずんだもんが「/zunda」で読み上げしてくれます。
APIキーの取得方法はこちらから。

LINE Messaging API側のWebhook

前述の通り、LINEBot側は2つ必要です。
それぞれにWebhookを設定します。

FIVE_SECOND側のLINEBotは

http://{railwayでのプロジェクト名}/FIVE_SECOND

FIVE_HOUR側のLINEBotは

http://{railwayでのプロジェクト名}/FIVE_HOUR

と設定します。

{railwayでのプロジェクト名}はDeploymentsで確認できます。

見れば分かると思いますがSERVER_NAMEで設定した名前がパスになります。

レスポンスが200(成功)と帰ってくるのを忘れずに確認しておきましょう。

LINE側を1つ、または3つ以上運用する場合

前述の通り、flask側のプログラムをいじる必要があります。
理由として、LINEBot1つにつき1個、エンドポイントを増やさなければならないからです。
(本当は自動生成させたかったんだけどエラー吐きまくってダメでした。)
なのでgit cloneでフォークしたテンプレをローカルに落とします。

ディレクトリをapp/serversへ移動させると

main_server.py
five_hour.py

の2つのPythonファイルがあると思います。
mainserver.pyがFIVE_SECOND側のLINEBotのプログラムで、five_hour.pyがFIVE_HOUR側のプログラムになります。

app/servers/main_server.pyの17行目から

from servers.five_hour import app2
from servers.bin.disreq import message_find,img_message,download

app = Flask(__name__)

app.register_blueprint(app2)


17行目は同階層のfive_hour.pyをインポートし、app2として扱います。
app.register_blueprint(app2)で分割されているプログラムを統合しています。
こうする事でLINEBotを分割しつつ二つホストしています。

1つだけ運用したい場合、説明した部分を削除し、環境変数SERVER_NAMEをFIVE_SECOND(カンマ入れない)にします。

3つ以上運用したい場合、このように追加していきます。

1 app/servers に新しいPythonファイルを作成します。
名前はなんでもいいですが、ここではC.pyにします。
2 C.pyに以下のコードを追加します。

from flask import request, abort, Blueprint ,current_app
import subprocess

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
   InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, ImageMessage, VideoMessage, StickerMessage
)
import os
from servers.bin.disreq import message_find,img_message,download
   
app3 = Blueprint("app3",__name__)

servers_name=os.environ['SERVER_NAME']
server_list=servers_name.split(",")

server_name=server_list[2]
    	
line_bot_api = LineBotApi(os.environ[f'{server_name}_ACCESS_TOKEN'])
handler = WebhookHandler(os.environ[f'{server_name}_CHANNEL_SECRET'])
    	
    
@app3.route(f"/{server_name}", methods=['POST'])
def callbacks():
		logger = current_app.logger
	    # get X-Line-Signature header value
		signature = request.headers['X-Line-Signature']
	

	    # get request body as text
		body = request.get_data(as_text=True)
		logger.info("Request body: " + body)
		#app2.logger.info("Request body: " + body)
	

	    # handle webhook body
		try:
			handler.handle(body, signature)
		except InvalidSignatureError:
			print("Invalid signature. Please check your channel access token/channel secret.")
			abort(400)
	

		return 'OK'


@handler.add(MessageEvent, message=[TextMessage,ImageMessage,VideoMessage,StickerMessage])
def handle_message(event:MessageEvent):
    event_type=event.message.type
    profile = line_bot_api.get_profile(event.source.user_id)

    if event_type=='text':
        message_text=event.message.text
    if event_type=='sticker':
        message_text=f"https://stickershop.line-scdn.net/stickershop/v1/sticker/{event.message.sticker_id}/iPhone/sticker_key@2x.png"
    if event_type=='image':
        # message_idから画像のバイナリデータを取得
        message_content = line_bot_api.get_message_content(event.message.id).content
        message_text=img_message(message_content)
    if event_type=='video':
        message_content = line_bot_api.get_message_content(event.message.id)
        download(message_content)
        message_text = subprocess.run(['python', 'upload_video.py', f'--title="{profile.display_name}の動画"','--description="LINEからの動画"'], capture_output=True)
    message_find(
        message_text,
        os.environ[f"{server_name}_GUILD_ID"],
        os.environ[f"{server_name}_TEMPLE_ID"],
        profile
    )


3 app/servers/main_server.pyにコードを追加します。

from servers.five_hour import app2
from servers.C import app3
from servers.bin.disreq import message_find,img_message,download

app = Flask(__name__)

app.register_blueprint(app2)
app.register_blueprint(app3)


4 環境変数SERVER_NAMEにCを追加します。

SERVER_NAME=FIVE_SECOND,FIVE_HOUR,C


5 LINEBot側のWebhookを指定します。

http://{railwayでのプロジェクト名}/C


以上です。4つ目以降も追加する場合も同様にやります。
ただし、
server_nameはserver_list[3],server_list[4]
app2,app3,app4
と追加するたびに変更するのを忘れずに。

完成!!

これで完成です。
多分やりとりできると思うのでなんか試しに送ってみましょう。