信頼はずっと、挑戦はもっと。

お問い合わせ
TEL:03-3496-3888

BLOG コアテックの社員ブログ (毎週月曜~金曜更新中)

2019

9

9月

slackコマンドっぽいことをchatworkでしたい!

テクログ

そろそろ夏も終わりですね。はやく涼しくなってほしいですね。


そういう気持ちから

slackコマンドっぽいことをchatworkでしたい!

ということになりました。


-----------------------

◆本当の経緯

もとからslackでのchatops的なものは結構やっていたんですが、

弊社slackだけではなくchatworkも使うんですね(ぼやかし


で、chatworkもWebhookがあって、

slackよりも機能や設定できることは少ないけど、

なんとか似たことができそうだな、となったので作りました。


-----------------------

構成

chatwork→APIGateway→

Tokenやコマンドを検証し、振り分けるLambdaFunction→(SNS)→コマンドの実態LambdaFunction+chatworkへ結果返答


わかりやすくするため

Tokenやコマンドを検証し、振り分けるLambdaFunction を [振り分けLambda]

コマンドの実態Lambda を [実態Lambda]

とします


-----------------------

設定を簡単に解説

細かい設定は書きませんが、いろいろ試行錯誤するのも経験のうちですよね。


(1)APIGatewayを作成

chatworkのWebhookが叩く入り口です。URLがあればひとまずOKです。

パス設定とかはおまかせします。


(2)[振り分けLambda]の殻を作成

APIGatewayの先のLambdaを作成します。


(3)APIGatewayを設定

(1)と(2)をつなぎまーす

叩くとレスポンスが来るだけ確認しておくのもいいですね。


(4)chatworkのWebhook設定

chatworkにボットアカウントを作り、

そのアカウントでWebhookの設定をします

参考:

https://help.chatwork.com/hc/ja/articles/115000169541-Webhook%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B


URLはAPIGatewayのURLで、イベントはルームイベントにしました。

(なぜなら、目に見えないところではなく、その部屋だけで使ってほしいからです。)

メッセージ作成のみ、にしました。


(5)[振り分けLambda]を実装

ソース!だよ!

何もライブラリが要らないってのを重視。

なんか貼ったら改行があれなのでまあそこはいいかんじにしてください。

何かあってもこちらはサンプルですのでそこはいつものとおりご了承ください。


見直すと入ってきた文字整形とか無理矢理感。

chatworkはクライアントごとにメッセージの形式が微妙に違ったりしてね…


必要な環境変数は


CHATWORK_API_TOKEN (KMSで暗号化してある、postに使うchatworkのToken

WEBHOOK_TOKEN (渡ってきたchatworkのメッセージが正しいものかを判定するためのToken


です。


# -*- coding: utf-8 -*-
# chatwork commanderの入り口
import boto3
import json
import logging
import os
import sys
from datetime import datetime
from botocore.exceptions import ClientError
import copy
import urllib.request, urllib.error
from base64 import b64decode, b64encode
import hmac
import hashlib

valid_command_list = ['こまんど1','こまんど2','こまんど3']

to_str = "[To:1111111111]"

usage_str = "【コマンド一覧】\n" + \

"※それぞれのコマンドの使い方については[コマンド usage]を打って確認してください\n\n" + \
"こまんど1\n" + \
"こまんど2\n" + \
"こまんど3\n"

SNS_ARN_PREFIX = "arn:aws:sns:ap-northeast-1:11111111111:chat_command_"

####
#メイン
####
def lambda_handler(event, context):
  #先にイベントは取っておく(判定とかに使うから
  eventjson = json.loads(json.dumps(event))
  bodyjson = json.loads(eventjson['body'])

  room_id    = str(bodyjson['webhook_event']['room_id'])
  message_id = str(bodyjson['webhook_event']['message_id'])
  body      = str(bodyjson['webhook_event']['body'])
  account_id = str(bodyjson['webhook_event']['account_id'])

  #まず
  #[To:1111111111]
  #がないなら無視(全メッセージ流れてくるから

  if not to_str in body:
    print('not me.END')
    return {'statusCode': 200,'body': json.dumps('not me.END')}

  #毎回kms使うのがやだからここでやってます
  ENCRYPTED_WEBHOOK_TOKEN = os.environ['WEBHOOK_TOKEN']
  WEBHOOK_TOKEN = boto3.client('kms').decrypt(CiphertextBlob=b64decode(ENCRYPTED_WEBHOOK_TOKEN))['Plaintext']
  ENCRYPTED_CHATWORK_API_TOKEN = os.environ['CHATWORK_API_TOKEN']
  CHATWORK_API_TOKEN = boto3.client('kms').decrypt(CiphertextBlob=b64decode(ENCRYPTED_CHATWORK_API_TOKEN))['Plaintext']

  #Tokenの確認
  if check_request(event, WEBHOOK_TOKEN) == True:
    print("Token OK")

    #body整形
    command_text = body.replace('\n', '').replace('[To:1111111]', '').replace('aaaaa さん', '').replace('aaaaaさん', '').strip()
    command_args = command_text.split(" ")

    #コマンドの確認
    command = command_args[0]
    nextStatus = checkCommandStrAndMakeNextStatus(command, room_id, account_id, message_id, command_text, CHATWORK_API_TOKEN)

    #nextStatusがTrue(続行)の場合、次のlambdaをSNSで呼ぶ
    #続行ではない場合は特に何もせずreturnして終わり
    if nextStatus == True:
      message = {"room_id"   : room_id,
            "command_args": command_args,
            "account_id" : account_id,
            "message_id" : message_id
      }
      #SNS_ARN_PREFIX+command がARNになるようになっている
      sendSNS(message, SNS_ARN_PREFIX+command)

    return {
      'statusCode': 200,
      'body': json.dumps('OK')
    }
  else:
    print("Token NG")
    return {
      'statusCode': 403,
      'body': json.dumps('NG')
    }

##-----------------------

def sendSNS(message, sns_TOPIC):
  sns_client = boto3.client(
    'sns'
  )
  #メッセージ整形
  message=json.dumps(message)
  message=json.dumps({'default': message, 'lambda': message})

  response = sns_client.publish(
    TopicArn=sns_TOPIC,
    Subject='/lambda',
    MessageStructure='json',
    Message=message
  )
  print(response)

####
#コマンドの内容を確認してchatwork通知
#また、今後の続行ステータスを返す
####
def checkCommandStrAndMakeNextStatus(command, room_id, account_id, message_id, command_text, CHATWORK_API_TOKEN):
  if command == 'usage':
    postChatwork(usage_str,room_id, CHATWORK_API_TOKEN)
    return False#終了
  else:
    if not command in valid_command_list:
      postChatwork("対象コマンドがありません。\n"+usage_str,room_id, CHATWORK_API_TOKEN)
      return False#終了
    else:
      #chatworkに受付状況を通知
      postChatwork(
        makeReplyMessage(account_id, room_id, message_id, command_text),
        room_id,
        CHATWORK_API_TOKEN
      )
      return True#続行

####
#メッセージをつけるだけ
####
def makeReplyMessage(account_id, room_id, message_id, command_text):
  return "コマンド:" + command_text + "\nを受け付けました。(コマンドの実態に渡しました)\nお待ち下さい。"

####
#chatworkに通知
####
def postChatwork(message, room_id, CHATWORK_API_TOKEN):
  print("in postChatwork")
  END_POINT_BASE = "https://api.chatwork.com/v2"
  END_POINT   = "/rooms/"
  ACTION     = "/messages"

  headers = { 'X-ChatWorkToken': CHATWORK_API_TOKEN }
  data = { 'body': message }
  data = urllib.parse.urlencode(data)
  data = data.encode('utf-8')

  # リクエストの生成と送信
  post_message_url = END_POINT_BASE + END_POINT + room_id + ACTION
  request = urllib.request.Request(post_message_url, data=data, method="POST", headers=headers)
  with urllib.request.urlopen(request) as response:
    response_body = response.read().decode("utf-8")
    print(response_body)

#参考
#https://qiita.com/ikedaosushi/items/b01183f207ea1db6a8d3
def check_request(event, WEBHOOK_TOKEN):
  webhook_sig = event['headers']['X-ChatWorkWebhookSignature'].encode('utf-8')
  sig = build_sig(event['body'], WEBHOOK_TOKEN)
  is_valid = webhook_sig == sig

  return is_valid

def build_sig(body, WEBHOOK_TOKEN):
  token_decode = b64decode(WEBHOOK_TOKEN)
  sig = hmac.new(token_decode, body.encode('utf-8'), hashlib.sha256).digest()
  sig_encoded = b64encode(sig)

  return sig_encoded


だいたいでいえば


・メッセージはルームのものがすべて来るので、処理対象メッセージなのかを判定

・tokenの確認

・コマンドの確認

・受け付けたことを返答

・SNSで次のLambdaを呼ぶ

(このSNSを一定の形式に統一することによって、後ろのコマンドを増やしやすくなってます。


(6)実態Lambdaを呼ぶためのSNSを作る

arn:aws:sns:ap-northeast-1:11111111111:chat_command_


ってなっているようにSNSをまず作ります

たとえばmake_EC2みたいなコマンドだった場合、

SNS名は

chat_command_make_EC2

になりますね。


(7)[実態Lambda]を作る

ここは自分でつくってね!

SNSを受け取って、分解して、処理をすればいいです。

終わったらchatworkに通知したり、いろいろ内部でやればいいですね。

夢が広がる。


(8)SNSの先に[実態Lambda]を追加

まあ追加するだけです



あとはchatworkでボットにはなしかけたりしてテストをしてみよう!


-----------------------


コマンドを増やすときも、

許可するコマンドを追加して、

SNS作成

コマンド実態Lambda作成

だけでぽんぽんふえるよ!


ルームを分けることによって、重要度ごとに使える人を分けられたりもします。



以上夏休みの自由研究にどうぞ。もうおわってますか。



-----------------------

<以下はただの一覧用の画像でーす>


この記事を書いた人

マスオさん

tzwy

所 属:
WEBインテグレーション事業部
出身地:
saitama
仕事内容:
いろいろ検証、作成、障害対応とか品質向上とか