Slackの絵文字をしっかりLINEスタンプ風にするAppを書いた

Slackの絵文字をしっかりLINEスタンプ風にするAppを書いた

前置き

チームの連絡手段をLINEのグループメッセージからSlackに移行するにあたって,スタンプがないのが寂しいという意見が寄せられたため,無理やりSlackに引き込むためにスタンプ機能を実装したよ,という話です.

やりたいこととしては,Slack AppでLINEスタンプに相当する機能を実装するというシンプルなものです.

関連事例

ネタとしてはn番煎じです. 同じことを考える方はいるようで,Web Hookを活用してGASで対応する画像URLを取得して貼り付けたり[1],CSSをいじったり[2][3],それをChromeの拡張機能として実装したり[4]と,絵文字を大きく表示させる方法がすでにいろいろ考えられています. しかし,[1]の場合は,画像のURLがメッセージ上に表示されてしまう上に,スタンプの情報を予めスプレッドシートに登録しておく必要があります. また,[2][3][4]の場合は,他人からの見え方をいじることができないことや,モバイルアプリでの見え方をコントロールできないことなどが問題となります.

目標と使うもの

そこで,この記事では以下のことを目標にして実装を行います.

  • URL等の無駄なものを表示させない
  • カスタム絵文字をアップロードするだけでスタンプを増やせるようにする
  • 通知にそれっぽい文字列を出す(「〜〜がスタンプを送信しました」みたいな)

SlackにはAPIが用意されていて,おまけにAPIを叩くためのライブラリも各言語向けに充実しています. 今回はFlaskを使ってPythonでプログラムを書き,zappaを使ってAWS Lambda上にデプロイしてサービスを走らせることにします.

今回AWSを初めて使ってみたので,変な表現や使い方の間違いがあるかもしれません.

準備

AWSを利用するために以下の準備を済ませておきます.

  • AWSのアカウント作成
  • IAMコンソールで認証情報の作成
  • AWS CLI等で認証情報ファイルの作成: 参考

これらについてはネットや書籍に腐るほど資料があるので,そちらを参照してください. 無料のKindle本もあるようですが,どうもAWSの英語版ヘルプを単に電子書籍化しただけのもののようです.

今回はArch Linux上で開発を行いました.使用したソフトウェアとライブラリのバージョンは以下のとおりです.

  • virtualenv 15.1.0
  • Python 3.6.1
    • zappa 0.41.2
    • Flask 0.12.1
    • slackclient 1.0.5

プロジェクト用にディレクトリを作って,その中にふつうにvirtualenvをつくります.

$ virtualenv env
$ source env/bin/activate

必要なパッケージをインストールします.

$ pip install flask zappa slackclient

zappaを試してみる

zappaって何よ

そもそもzappaとはなんぞやというのを簡単に解説します.

Zappa makes it super easy to build and deploy all Python WSGI applications on AWS Lambda + API Gateway. Think of it as “serverless” web hosting for your Python apps. That means infinite scaling, zero downtime, zero maintenance - and at a fraction of the cost of your current deployments!

ZappaREADME.md

zappaを使うと超簡単にWSGIアプリケーションを AWS Lambda + API Gateway にビルド・デプロイできます. サーバレスになるので,メンテナンスもダウンタイムもありませんし,スケーリングも自由自在,しかも費用は何分の1にもなります.

というようなことが書かれています. Pythonでウェブサービスを書こうとすると,だいたいFlaskやDjangoのようなWSGIフレームワークで作成するわけですが,通常のやり方でこいつらを動かすためにはPythonが常時起動できるサーバを用意する必要があります. しかしzappaを使えばその必要はありません. 既存のWSGIアプリケーションを,コードはそのままに,AWS Lambda + API Gateway に展開できるわけですからこれはすごいです. AWSはかなりのリソースを無料で開放してくれていますから,こんなのを見つけちゃうとVPSとか契約するのが正直ばからしくなっちゃいます.

というわけで,これを使ってSlackのカスタム絵文字をLINEスタンプ風にするアプリを走らせます.

zappaを使う(Hello World)

FlaskでHello Worldを表示するだけのアプリケーションを,zappaを使ってデプロイしてみようと思います.

使用するコードはこちら(コード引用元):

app.py

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()

試しに手元で実行してみましょう.

$ python app.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

$ curl localhost:5000
Hello World!

これをAWSに乗っけるために,zappaのセットアップをします.プロジェクトのディレクトリでつぎのコマンドを実行します.

$ zappa init

ひとまず上記の画像のように何も入力せずに,デフォルトの設定を適用します. zappa init によって,zappaの設定ファイルであるzappa_settings.jsonが作成されます.

$ ls
app.py  env  zappa_settings.json

あとは zappa deploy dev するだけで勝手にデプロイしてくれます.恐ろしい手軽さ…!

$ zappa deploy dev
Calling deploy for environment dev..
Downloading and installing dependencies..
100%|█████████████████████████████████████████████████████████████████████████████████| 39/39 [00:04<00:00, 10.68pkg/s]
Packaging project as zip..
Uploading zappatest-dev-1494659931.zip (6.1MiB)..
100%|█████████████████████████████████████████████████████████████████████████████| 6.44M/6.44M [00:00<00:00, 7.96MB/s]
Scheduling..
Scheduled zappatest-dev-zappa-keep-warm-handler.keep_warm_callback!
Uploading zappatest-dev-template-1494659940.json (1.6KiB)..
100%|█████████████████████████████████████████████████████████████████████████████| 1.63K/1.63K [00:00<00:00, 4.27KB/s]
Waiting for stack zappatest-dev to create (this can take a bit)..
 75%|██████████████████████████████████████████████████████████████▎                    | 3/4 [00:15<00:06,  6.20s/res]
Deploying API Gateway..
Deployment complete!: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev

何かエラーが起きる場合は,aws configure やIAMコンソールの認証情報などを確認してみてください.

先ほどと同様に curl でURLを叩いてみると,Hello World! が表示されます.

備考

  • アプリケーションを編集して反映させたい場合は,deploy ではなく zappa update dev を実行してアップデートします.
  • zappa tail dev を実行するとログが流れてきます.かなりカラフル

Slack Appを作ってデプロイする

皆さんご存知のとおり,SlackはBotやAppによって機能の拡張が簡単にできます. 今回の絵文字をスタンプ化する機能は,Events APIでメッセージのイベントを取得し,絵文字を判定して,絵文字画像のURLをattachmentに入れて投稿する,という処理で実現できます.

コードはGitHubで公開しています. セットアップの手順はREADMEをご覧ください.

基本的にはSlack Events API Pythonライブラリのサンプルコードを参考にして実装しました.

今回はこのライブラリは使用せずに,slackclientとFlaskだけで実装を行い,zappaを使ってAWS Lambdaにデプロイします. ここから先は,今回実装したコードの一部分を示しながら,Events APIを使用したSlack Appの実装と,zappaでのデプロイ手順を説明していきます

Step 1. Slack Appの登録

Slack APIのApp管理ページへ行き,Appを作成します. App Nameはこの記事ではとりあえず Gigamoji とし,アプリを利用するチームを選択して完了です.

Step 2. イベント受取の設定・Slack API認証鍵のチェック

"Event Subscriptions"を開き,メッセージ関係のイベントを受け取るように設定します. "Enable Events"をONにして,“Add Team Event” から取得するイベントを選択します. 今回は4つ設定していますが,プライベートチャンネルやDMでスタンプ化しない場合は message.channels 以外追加する必要はありません. 変更したら,画面下の “Save Changes” を押して変更を保存します.

今回のアプリケーションは投稿されたメッセージを読むほかに,絵文字の情報にアクセスしたりユーザとして投稿を行ったりするため,"OAuth & Permissions"で emoji.readchat.write.user の権限を追加する必要があります.

最後に,"Basic Information"を開いてClient ID, Client Secret, Verification Tokenの3つを手元にコピーしておきます.

これらの秘匿すべき情報は,アプリケーションを実行するサーバの環境変数に格納するのが定石になっているようです. この記事でもその方法に則って,環境変数から各種認証情報を取得します.

# 認証情報は環境変数に置く
SLACK_VERIFICATION_TOKEN = os.environ["SLACK_VERIFICATION_TOKEN"]
SLACK_CLIENT_ID = os.environ["SLACK_CLIENT_ID"]
SLACK_CLIENT_SECRET = os.environ["SLACK_CLIENT_SECRET"]

zappaを使ったAWS Lambdaのリモート環境変数の設定方法は Step 7 で解説します.

Step 3. チャレンジに対するレスポンスの実装

Slack Events APIを使用するためには,url_verification というイベントに対して指定された値で返答するようにしなければなりません[5]. これを実現するのがつぎのコードです(残りの部分はGitHubのコードを参照).

@app.route("/", methods=['POST'])
def root():
    data = request.get_json(force=True)

    # このリクエストがSlackからきたものかどうかを簡易的に判定
    if ('type' not in data) or ('token' not in data) or (data['token'] != SLACK_VERIFICATION_TOKEN) :
        return die_noretry(400)

    if data['type'] == 'url_verification':
        return Response(data['challenge'], mimetype='application/x-www-form-urlencoded', status=200)

(2018.Mar.23 修正.force=True しないとパースできなくなっていた.原因は不明.)

これを実装した状態でデプロイしたら,"Event Subscriptions"の"Request URL"にアプリケーションのURLを貼り付けて,Appを登録します. ここで入力したURLにイベントが飛んできます.

Step 4. OAuth認証・DynamoDBへのユーザトークンの保存

"OAuth & Permissions"を開いて,"Install App to Team"ボタンをクリックします. 承認画面が出てくるので,"Authorize"ボタンをクリックして,Appをチームにインストールします.

つぎに, “Redirect URLs” の項目を設定します. ここでは,デプロイ先の https://xxxxxxxxx.execute-api.xxxxxxx.amazonaws.com/dev/auth_callback を指定します.

ユーザの承認後,Redirect URLに対してOAuthアクセスのためのコードが渡されるので,Slack APIの oauth.access メソッドを呼んでユーザに紐付いたトークンを取得し,それをDynamoDBに保管することにします. まず保管先のDynamoDBのテーブルをAWSのコンソールで作成します.

テーブル名を適当に設定します. 今回は,Primary KeyとしてチームIDとユーザIDを組み合わせた文字列を使用し,値はOAuthトークンを UserToken に格納します. Primary Keyの名前はここでは TeamUserId としておきます. キャパシティは適当に設定します.

テーブルの作成が完了したので,つぎにプログラムからDynamoDBを使用するコードを書きます. テーブル名とAWSリージョンは環境変数に格納しておきます(リージョンはboto3を使って取得できそうな気もしますが,今回はひとまず). DynamoDBはPython向けライブラリから非常に簡単に操作できるようになっています.

AWS_REGION = os.environ["AWS_REGION"]
DYNAMODB_TABLE_NAME = os.environ["DYNAMODB_TABLE_NAME"]

dynamodb = boto3.resource('dynamodb', region_name=AWS_REGION)
table = dynamodb.Table(DYNAMODB_TABLE_NAME)

# ... 中略 ...

@app.route("/auth_callback", methods=["GET"])
def auth_callback():
    # OAuthコードの取得
    auth_code = request.args.get('code')

    # OAuth認証のための一時的なSlackクライアントの生成(トークン必要なし)
    slack_client = SlackClient("")

    # OAuth認証し,ユーザトークンを取得
    auth_response = slack_client.api_call("oauth.access", client_id=SLACK_CLIENT_ID, client_secret=SLACK_CLIENT_SECRET, code=auth_code)
    user_token = auth_response.get("access_token")

    # ユーザに紐付いたSlackクライアントの生成
    slack_client = SlackClient(user_token)
    # チーム名,チームIDとユーザIDを取得
    response = slack_client.api_call("auth.test", token=user_token)
    team_name = response.get("team")
    team_id = response.get("team_id")
    user_id = response.get("user_id")

    # 古いアイテムは消しておく.ユーザトークンをDynamoDBに登録
    table.delete_item(Key={'TeamUserId': team_id+user_id})
    table.put_item(Item={'TeamUserId': team_id+user_id, 'UserToken': user_token})

    return "Gigamojiを<b>%s</b>にインストールしました!!" % (team_name)

oauth.access でユーザのトークンを取得し,auth.test でチームIDとユーザIDをあらためて取得,DynamoDBに保存という流れです. 今回は平文でてきとーに保存していますが,必要に応じて暗号化するようにしたほうがよさそうです.

Step 5. Appのインストール画面の作成

今回のアプリケーションは,Appがユーザのメッセージを消したりユーザの代わりに投稿したりするものです. したがって,チームにAppをインストールするだけでなく,ユーザごとにインストール画面を開いてAppの認証を行う必要があります. ここではその画面を作成します.めんどくさいので単純に “Add to Slack” ボタンだけです.

@app.route("/", methods=['GET'])
def pre_auth():
    add_to_slack = """
        <a href="https://slack.com/oauth/authorize?&client_id=%s&scope=chat:write:user,emoji:read,channels:history,groups:history,im:history,mpim:history">
            <img alt="Add to Slack" src="https://platform.slack-edge.com/img/add_to_slack.png"/>
        </a>
    """ % SLACK_CLIENT_ID
    return add_to_slack

このボタンを押してユーザが承認すると,Step 4で設定したURLに認証に必要な情報が飛んでいきます.

Step 6. イベントを受け取って処理する

これで準備ができたので,イベントを受け取って処理するコードを書きます. イベントのデータは type 属性に "event_callback" が指定されていて,JSON形式で “/” 宛にPOSTで届きます.

  1. message イベントであることを確認
  2. メッセージが絵文字のみかどうかをチェック
  3. DBからユーザのトークンを取得
  4. 絵文字の画像URLを取得
  5. もとのメッセージを消してスタンプ化した絵文字をユーザとして投稿する

の手順で処理を行います.

普通にURLを投稿するだけではスタンプ化には程遠いので,Attachment機能[6]を利用して画像だけをうまく表示させるようにします. 画像のみを表示するためには text 属性を空白にしてAttachmentの image_url にURLを指定します. また,fallback 属性に指定した文字列は,text が利用できないときの代替文字列として通知に出してくれるみたいです.

ということで,今回は以下のようなAttachmentを使用します.

{
  "attachments": [
    {
      "fallback": "スタンプを送信しました",
      "image_url": "https://fst.slack-edge.com/0e8da/img/emoji_2016_06_08/apple/simple_smile.png"
    }
  ]
}

Message Builderを使うと,どのように表示されるかインタラクティブに確かめることができます: Message Builderの使用例

これで材料は整ったので,あとは手順通りにプログラムを組むだけです.ライブラリがしっかりしているのでらくちん.

def event_callback(data):
    data = request.get_json()

    # messageイベントで,かつメッセージの削除・編集等ではないことを確認
    if data["event"]["type"] != "message" or "subtype" in data["event"]:
        print("event type mismatch")
        return die_noretry(400) # Bad Request

    team_id = data["team_id"]
    user_id = data["event"]["user"]

    # メッセージのテキストを取得,絵文字を切り出す
    message = data["event"]
    emoji_re = ':[a-z0-9\-_]+:'
    emoji_in_text = re.findall(emoji_re, message["text"])
    rest_text = re.sub(emoji_re, '', message["text"]) # 残りのテキスト

    # メッセージの中身が絵文字だけかの判定
    if len(emoji_in_text) != 1 or (rest_text and not rest_text.isspace()):
        print("emoji condition not met")
        return die_noretry(400) # Bad Request

    # ユーザのトークンをDynamoDBテーブルから取得
    response = table.get_item(Key={'TeamUserId': team_id+user_id})
    # Slackクライアントを生成
    slack_client = SlackClient(response['Item']["UserToken"])

    # 絵文字の画像URLを取得
    emoji_list_response = slack_client.api_call("emoji.list")
    emoji_name = emoji_in_text[0].strip(":")
    emoji_url = emoji_list_response["emoji"].get(emoji_name)
    if emoji_url is None:
        print("emoji url is not set")
        return die_noretry(400)

    # もとのメッセージを消して,スタンプ化した絵文字を投稿する
    channel = message["channel"]
    att = [{ "fallback": "スタンプを送信しました", "image_url": emoji_url }]
    slack_client.api_call("chat.delete", channel=channel, ts=message["ts"], as_user=True)
    slack_client.api_call("chat.postMessage", channel=channel, text="", attachments=json.dumps(att), as_user=True)

    return Response("")

ここで,200番台以外のステータスコードを返すとSlack側がリトライを投げてくるので,それを防ぐために X-Slack-No-Retry をヘッダに設定して無効化するようにします. 全部200で返してもいいんですけど,まあ…

def die_noretry(code):
    # X-Slack-No-Retry をヘッダに設定して,エラー時のリトライを無効化
    return Response(headers={"X-Slack-No-Retry": 1}, status=code)

@app.errorhandler(500)
def internal_server_error(error):
    return die_noretry(500)

Step 7. zappaでのリモート環境変数の設定,デプロイ

そんなこんなでコードが準備できたので,zappaを使ってデプロイします.

zappaでは,S3 Bucketに環境変数を記述したjsonファイルを置くことでリモート環境変数を設定できるようになっています. まずはファイルを書きます.

env.json

{
    "SLACK_VERIFICATION_TOKEN": "xxxxxxxxxxxxxxxxxxxxxxxx",
    "SLACK_CLIENT_SECRET": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "SLACK_CLIENT_ID": "000000000000.000000000000",
    "DYNAMODB_TABLE_NAME": "xxxxxxxxxxxxxxxxxxx",
    "AWS_REGION": "ap-northeast-1"
}

S3 Bucketを新しく作成し,このファイルをアップロードします. zappa_settings.json の remote_env 属性にファイルの場所を指定すれば設定完了です. 最終的にこんな感じになります.

zappa_settings.json

{
    "dev": {
        "app_function": "gigamoji.app",
        "aws_region": "ap-northeast-1",
        "profile_name": "default",
        "s3_bucket": "zappa-xxxxxxxxx",
        "keep_warm": false,
        "remote_env": "s3://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/env.json"
    }
}

満を持してデプロイです.すでに完了している場合は update で既存のものをアップデートしてください.

$ zappa deploy dev
もしくは
$ zappa update dev

ここで初めてデプロイした場合は,Step 3, 4のURLの設定を忘れないようにしてください.

使ってみる

認証

デプロイ先のURLにアクセスするとボタンが出てくるので承認します.

Authorizeボタンを押すと,“Gigamojiを[チーム名]にインストールしました!!” と表示されて登録が完了します.

Slackでの動作確認

このアプリケーションはカスタム絵文字のみに対応しています(標準の絵文字はURLが取得できないので). てきとーなチャンネルで絵文字だけを投稿して動作確認をしてみると,つぎのようになります.

Androidアプリではファイルサイズも出ないので,スタンプっぽさが増します. 絵文字のほかに文字列がある場合や,1つの投稿に絵文字が2つ以上含まれている場合はスタンプ化されません.

また,通知の文字列もきちんと設定できています(別ユーザから観測).

まとめ

  • Slack Appで絵文字をスタンプ化してみた
  • zappaを使えばWSGIアプリケーションをサーバレス化できる
    → VPSやSaaSホストの準備が要らなくなる
  • AWSの利用方法の勉強になった

  1. Slackの絵文字をLINEのスタンプみたいに大きく使えるようにしてみた | icchi’s blog

  2. SlackでLINEみたいなスタンプを使いたいと言われたので - Qiita

  3. SlackでLINEスタンプっぽいことをするChrome Extension作りました - mnkk

  4. shirayuca.github.io | Slackで自動スタンプ風

  5. url_verification event | Slack

  6. Attaching content and links to messages | Slack

Share Comments