3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PythonでLINE WORKS版 Trello Botを作るまでのお話

Last updated at Posted at 2019-06-03

某ICT関連企業で人事(教育・研修担当)をやっている@0yanと申します。
初めてのQiitaへの投稿ですので至らぬ点も多々あるかと存じますが、宜しくお願い致します。

LINE WORKS版 Trello Botを作った経緯

私の部署では、Trelloを使ってタスク共有しているのですが、会社で使っているチャットツールのLINE WORKSにTrelloの更新通知機能がないため、仕方なくHangouts ChatのTrello Botを使ってタスク共有しておりました。

しかし、普段のやり取りはLINE WORKSを見て、タスク更新のお知らせが来たらHangouts Chatを見て・・・というのは正直めんどくさい。
そのため、Pythonの勉強がてら自分でTrello Botを作ることにした次第です。

なお、プログラミングは独学で学んでいる最中でして、学習期間としてはJavaが3ヶ月(基本情報技術者試験でJavaを選択)、Pythonが1ヶ月半です。
そのため、コード汚いところがあると思います。
お気づきの点がございましたら、ご指摘頂けますと幸いです。

概要

Trelloのカードにコメントが投稿されたら更新通知(JSON)をWebhookで受け取る。
JSONのコメント部分だけ切り出したうえで、LINE WORKSのトークルームに通知する。
そんなBotを作りました。
なお、今回はローカルPCをWebサーバ化してngrokで外部公開し、Botがきちんと動作するところまでを確認しております。

2019/8/14追記:Herokuにアップロードするまでの記事を書きましたので、よければ参考にしてください。

LINE WORKS用Trello BotをHerokuにデプロイするまで

2019/8/23追記:PyPIに公開したライブラリを使って楽にBot実装できるようにしました。よければご覧ください。

【備忘録】PythonによるLINE WORKS版Trello Botの実装(PyPI lineworks インストールVer.)

環境

  • Windows 10 Home
  • Python 3.6.8(Anaconda3)
  • Flask 0.12.2
  • gunicorn 19.9.0
  • virtualenv 16.6.0
  • py-trello 0.15.0

Bot作成の大まかな流れ

  1. LINE WORKS API情報取得
  2. LINE WORKS トークBot登録
  3. コーディング①(トークBot)
  4. Trello API情報取得
  5. コーディング②(Trello Webhook)
  6. コーディング③(Webhookサーバ)
  7. ローカルPCをWebサーバ化してngrokで外部公開

1. LINE WORKSのAPI情報取得

1番から3番まではitoxさんの記事と、LINE WORKSの公式ドキュメントを参考にしました。

itoxさんの記事:PythonでJWT生成からボット作成、投稿までやってみた
URL:https://www.slideshare.net/itoxdev/pythonjwt

LINE WORKS Developers>概要
URL:https://developers.worksmobile.com/kr/document/13?lang=ja

最初に行うのは、公式ドキュメントのAPI共通ガイド>API認証の準備の部分です。
LINE WORKS Developersの右上にあるログインボタンを押下しログイン後、以下四点を順次発行していきます。

  • API ID
  • Server API Consumer Key
  • Server List(ID登録タイプ) ID
  • Server List(ID登録タイプ) 認証キー

image.png
image.png
image.png

2. LINE WORKS トークBot登録

Developer Consoleのサイドバー「Bot」をクリックし、遷移した画面の「登録」ボタンを押下するとBot登録画面になります。
image.png
この画面で、下表のとおり、登録していきます。

項目  内容 備考
Bot名  Trello Bot 違う名前でも可 
「説明」  任意の説明文  
Callback URL  ラジオボタンOff 今回はメッセージ送信のみのため 
Botポリシー  チェックボックスON Botに、複数人のトークルームへのTrello更新通知を送信させたいため 
管理者(主担当)  開発者のアカウント 組織の事情に合わせて変更可 

3.コーディング①(トークBot)

ライブラリとグローバル変数

ライブラリとグローバル変数は以下の通りです。
なお、1で発行したServer List(ID登録タイプ)の認証キーのファイルについては、コーディングするファイル(bot.py)と同じ階層に格納します。

bot.py
# -*- cording:utf-8 -*-

import base64
import datetime
import json
import requests

from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA


# グローバル変数
API_ID = "1で発行したAPI ID"
SERVER_ID = "1で発行したServer List(ID登録タイプ)のID"
KEY_FILE_NAME = "1で発行したServer List(ID登録タイプ)の認証キーのファイル名"
# APIトークンを格納する変数(後で使用)
API_TOKEN = ""
SERVER_API_CONSUMER_KEY = "1で発行したServer API Consumer Key"
DOMAIN_ID = "LINE WORKS Developer Consoleのサイドバー下部に記載のID"
BOT_NO = "2で登録したトークBotのBot No."
ACCOUNT_ID ="開発者のID(テスト用。トーク画面の個人情報から見れます)"

Botクラスの全体像

ここからBotクラスを作っていきますが、LINE WORKSのAPIを叩くには公式ドキュメント>API共通ガイド>API認証の準備に記載のとおり、Server Tokenを発行しなければなりません。
image.png
出典:Works Mobile Corp.(発行年不明)「API認証の準備」,https://developers.worksmobile.com/kr/document/1002002?lang=ja2019年6月2日アクセス.

そのため、BotクラスにはTrello通知を送るためのメッセージ送信メソッド以外のメソッドも複数含まれております。
具体的には以下の通りです。

メソッド名 内容
dict_to_base64str 辞書型文字列からBASE64Unicode文字列にエンコード(jwt_createメソッドで使用)
sign_rsa RSA SHA-256アルゴリズムで暗号化し、BASE64エンコード(jwt_createメソッドで使用)
jwt_create JWTの生成(上図:JWT作成及び電子署名)
get_server_token サーバートークンの取得(上図:JWTを使ってLINE WORKS認証サーバーにToken要請)
call_server_api サーバーAPIの呼び出し(上図:Tokenを使ったServer Api使用)
send_message 汎用メッセージ送信(content_dictにテキスト以外のタイプを格納)
send_text_message テキストメッセージ送信(テキストメッセージをaccountIdまたはroomIdに送信)

Botクラスのコーディング

①JWTの生成に必要なメソッド

上表に記載の通り、JWTの生成にあたり、

  • BASE64 Unicode文字列へのエンコード
  • RSA SHA-256アルゴリズムで暗号化したうえでBASE64エンコード
    を行う必要があるため、そのメソッドをコーディングします。
bot.py
class Bot(object):
    def dict_to_base64str(self, dict):
        # 辞書型文字列をJSON形式にエンコード
        dump_text = json.dumps(dict)

        # JSON文字列をUnicode文字列にエンコード後、BASE64エンコードしたうえで、Unicode文字列にデコード
        base64_text = base64.urlsafe_b64encode(dump_text.encode('utf-8')).decode('utf-8')

        return base64_text

    def sign_rsa(self, message):
        global KEY_FILE_NAME

        # Server List(ID登録タイプ)で発行した認証キーのファイルを読み込み
        key = RSA.importKey(open(KEY_FILE_NAME).read())

        # Unicode文字列をバイトコードに直す
        message_byte = message.encode('utf-8')

        # ハッシュ値の計算
        digest = SHA256.new(message_byte)

        # 鍵を基にしてPKCSのインスタンス生成
        signer = PKCS1_v1_5.new(key)

        # 署名を作成(The signature encoded as a string.:bytes型のstring)
        signature = signer.sign(digest)

        # 署名のバイトデータのままBASE64エンコードし、それをUnicode文字列にデコード
        return base64.urlsafe_b64encode(signature).decode('utf-8')
②JWTの生成

①の準備が出来たら、JWTの生成メソッドをコーディングします。

bot.py
    def jwt_create(self):
        global SERVER_ID

        jwt_header = {
            "alg": "RS256",
            "typ": "JWT"
        }
        jwt_header_base64 = self.dict_to_base64str(jwt_header)

        json_claim_set = {
            # Server List(ID登録タイプ)で発行したサーバーID
            "iss": SERVER_ID,
            # JWT生成日時 UNIX時間で指定(単位:sec)
            # "iat": int(datetime.datetime.now().strftime('%S')), ←うまくいかないので修正
            # timestampメソッドはfloat型で返すので、int型に直している
            "iat": int(datetime.datetime.now().timestamp()),
            # JWT満了日時 UNIX時間で指定(単位:sec)
            # "exp": int((datetime.datetime.now() + datetime.timedelta(minutes=30)).strftime('%S')) ←同上
            "exp": int((datetime.datetime.now() + datetime.timedelta(minutes=30)).timestamp())
        }
        json_claim_set_base64 = self.dict_to_base64str(json_claim_set)

        # {header BASE64 エンコード}.{JSON Claim set BASE64 エンコード}
        plain_text = jwt_header_base64 + "." + json_claim_set_base64

        # ②JWT電子署名
        # プレーンテキストを秘密鍵で暗号化し、BASE64Unicode文字列を得る
        signature_base64 = self.sign_rsa(plain_text)

        # 結合してJWTを得る
        jwt = plain_text + "." + signature_base64

        return jwt
③サーバートークンの取得

JWT生成メソッドの準備が出来たら、次はサーバートークンを取得するメソッドです。

bot.py
    def get_server_token(self):
        global API_ID

        api_url = 'https://auth.worksmobile.com/b/{}/server/token'.format(API_ID)

        headers = {
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        }

        payload = {
            "grant_type": "urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer",
            "assertion": self.jwt_create()
        }

        res = requests.post(url=api_url, headers=headers, params=payload)

        access_token = json.loads(res.text)["access_token"]
        print("API_TOKEN:" + access_token)

        return access_token
④サーバーAPIの呼び出し

取得したサーバートークンを使ってサーバーAPIを呼び出すメソッドです。

bot.py
        global API_ID
        global API_TOKEN
        global SERVER_API_CONSUMER_KEY

        # トークンを取得
        API_TOKEN = self.get_server_token()

        # LINE WORKSのAPIキー設定
        API_URL = "https://apis.worksmobile.com/{}/{}".format(API_ID, resource)

        headers = {
            "Content-Type": "application/json; charset=UTF-8",
            "consumerKey": SERVER_API_CONSUMER_KEY,
            "Authorization": "Bearer " + API_TOKEN,
        }

        r = requests.post(API_URL, headers=headers, data=json.dumps(payload))
        return r.text
⑤メッセージ送信

LINE WORKSでは、テキスト以外にもテンプレート等を送ることができるため、後々のことを考えてコンテンツを入れ替えられるよう、二つのメソッドに分割しております。

bot.py
    def send_message(self, botNo, accountId, roomId, content_dict):
        resource = "message/sendMessage/v2"
        payload = {
            "botNo": botNo,
        }
        # accountIdとroomIdはどちらか一方を指定
        if accountId != "":
            payload.update({"accountId": str(accountId)})
        elif roomId != "":
            payload.update({"roomId": str(roomId)})
        else:
            print("accountId and roomId error.")
        payload.update(content_dict)

        res = json.loads(self.call_server_api(resource, payload))
        result = str(res["code"]) + " " + res["message"]
        print("send_message:" + result)
        return result

    def send_text_message(self, botNo, accountId, roomId, send_text):
        content_dict = {
            "content": {
                "type": "text",
                "text": send_text,
            }
        }
        return self.send_message(botNo, accountId, roomId, content_dict)

以上でトークBotのコーディングは終了です。

4. Trello API情報取得

こちらについては、Issei Komatsuさん(@isseium)の記事を参考にさせて頂きました。
ここをクリックするとAPI情報を取得できます。

タイトル:Trello API を叩いてカードを作成する方法(curl利用)
URL:https://qiita.com/isseium/items/8eebac5b79ff6ed1a180

5. コーディング②(Trello Webhook)

てぃるとさんの記事を参考にさせて頂きました。

タイトル:Python3 + Flask + py-trello + slackerでTrelloの更新情報をslackに流す(1)
URL:http://makemove.hatenablog.com/entry/2017/09/17/191507

Trelloのカードを更新したら、JSON形式のデータをコールバックURLに投げるWebhookを作ります。
なお、コールバックURL自体は、次のWebhookサーバのコーディングが終わり、ngrokで外部公開してから入力します。
初心者なのでここでハマったのですが、ファイアウォール内のローカルPCをコールバックURLに指定することはできないためです。

create_hook.py
# -*- coding: utf-8 -*-

from trello import TrelloClient
from trello import Board

def createhook():
    # Trello API情報
    api_key = "4番で取得したAPI Key"
    token = "4番で取得したToken"
    api_secret = "4番で取得したAPI Secret"

    # クライアントインスタンス生成(API認証)
    client = TrelloClient(api_key=api_key, api_secret=api_secret, token=token)

    # コールバックURLの指定
    callback_url = "7番でローカルPCのWebサーバを外部公開してから入力"

    # ボードクラスのインスタンス生成
    board = Board(client=client, board_id="通知を飛ばしたいボードのID")

    # webhookの作成
    webhook = client.create_hook(callback_url=callback_url, id_model=board.id)

    # webhookが作成されたか確認
    if webhook == False:
        print("Failed.")
    else:
        print("Success!")

# 7番でローカルPCを外部公開し、コールバックURLを入力した後に実行、Webhook作成する
if __name__ == '__main__':
    createhook()

6. コーディング②(Webhookサーバ)

こちらも5番同様、てぃるとさんの記事を参考にさせて頂きました。

webhook_server.py
# -*- coding: utf-8 -*-

from flask import abort
from flask import Flask
from flask import request

from bot import Bot


# LINE WORKS(LW)必要情報
API_TOKEN = ""
BOT_NO = "2で登録したトークBotのBot No."
ACCOUNT_ID ="開発者のID(テスト用。トーク画面の個人情報から見れます)"


app = Flask(__name__)


# appの動作確認用
@app.route('/')
def index():
    return 'Hello World!'


# Webhook
@app.route('/webhook', methods=['GET', 'POST', 'HEAD'])
def webhook():

    # LWのメッセージ送信に必要な情報
    global API_TOKEN
    global ACCOUNT_ID
    global BOT_NO

    # webhookのメイン処理
    elif request.method == 'POST':

        # Trelloカードの更新タイプ
        action_type = request.json['action']['display']['translationKey']

        # アクションタイプがコメントの場合のみ、メッセージ送信
        if action_type == 'action_comment_on_card':
            card_name = request.json['action']['data']['card']['name']
            user_name = request.json['action']['memberCreator']['fullName']
            comment = request.json['action']['data']['text']
            message = user_name + "さんが「" + card_name + "」にコメントしました。\n" + "コメント:" + comment
            bot = Bot()
            bot.send_text_message(botNo=BOT_NO, accountId=ACCOUNT_ID, roomId="", send_text=message)
            return '', 200
        else:
            pass
        return '', 200

    # Trello webhook登録時にHEADで200を返す必要があるため用意する
    elif request.method == 'HEAD':
        return '', 200
    else:
        abort(400)

if __name__ == '__main__':
    app.run(port=5000, host='127.0.0.1')

7. ローカルPCのWebサーバをngrokで外部公開

@mininobuさんの記事を参考にさせて頂きました。

タイトル:ngrokが便利すぎる
URL:https://qiita.com/mininobu/items/b45dbc70faedf30f484e

ngrokコマンドを実行し、外部公開用のURLの払い出しが成功してコンソールに表示される画面内に、「Forwarding  http://XXXXXXXX.ngrok.io -> localhost:8080」という表示があります。
このURLをcreate_hook.pyのコールバックURLに指定した後、同ファイルのcreate_hookメソッドを実行します。
Webhook作成に成功したら、コンソールに「Success!」の表示がされますので、Trelloの適当なカードでコメントに入力してみてください。
下図のように通知が来たら成功です!

image.png

さいごに

初めての投稿且つプログラミング初心者が見様見真似で書いたので至らぬ点も多々あったかと思いますが、私と同じように「LINE WORKSでTrelloの通知を受け取りたい!」という方のお役に立てば幸いです。
長文にも関わらず、ご覧いただき誠にありがとうございました。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?