LoginSignup
2
0

【LINE WORKS】rubyでオウム返しBotを作る

Posted at

はじめに

前回、AWS SAMのサンプルを実行してたので、今回はそれを利用してline worksのオウム返しbotを構築した備忘録になります

前回の記事はコチラ

初期設定

まずは、環境の新規構築を行います

コマンド
$ sam init
$ 省略
Project name [sam-app]: oumu
$ 省略
$ cd oumu
$ mv hello_world/ oumu/

テンプレート改修

oumu/template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  oumu

  Sample SAM Template for oumu

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3
    MemorySize: 128

Resources:
- HelloWorldFunction:
+ OumuFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
-     CodeUri: hello_world/
+     CodeUri: oumu/
      Handler: app.lambda_handler
      Runtime: ruby3.2
      Architectures:
        - x86_64
      Events:
-       HelloWorld:
+       OumuApi:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
-           Path: /hello
+           Path: /oumu
-           Method: get
+           Method: post
            
Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
- HelloWorldApi:
+ OumuApi:
-   Description: "API Gateway endpoint URL for Prod stage for Hello World function"
+   Description: "API Gateway endpoint URL for Prod stage for oumu function"
-   Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
+   Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/oumu/"
- HelloWorldFunction:
+ OumuFunction:
-   Description: "Hello World Lambda Function ARN"
+   Description: "oumu Lambda Function ARN"
-   Value: !GetAtt HelloWorldFunction.Arn
+   Value: !GetAtt OumuFunction.Arn
- HelloWorldFunctionIamRole:
+ OumuFunctionIamRole:
-   Description: "Implicit IAM Role created for Hello World function"
+   Description: "Implicit IAM Role created for oumu function"
-   Value: !GetAtt HelloWorldFunctionRole.Arn
+   Value: !GetAtt OumuFunctionRole.Arn

デプロイ

正常に動くか動作検証がてらにデプロイを実施します

コマンド
$ sam build
$ sam deploy --guided

省略

Successfully created/updated stack - oumu in us-west-2

動作確認

コマンド
$ curl -X POST 'デプロイされたエンドポイント'
{"message":"Hello World!"}

これで、postのAPIが正常に動いてるのが分かります
エンドポイントは次でも利用するのでメモしといてください

line worksの準備

こちらの記事に詳細が書いてあるので、こちらを参考にしてください(botの発行などは割愛)

bot作成時にコールバックを設定

image.png

こちらに、samでデプロイ時に出力されたエンドポイントを登録してください

開発開始

コマンド
$ sam sync --watch --profile hogehoge

省略

Stack update succeeded. Sync infra completed.            
CodeTrigger not created as CodeUri or DefinitionUri is missing for ServerlessRestApi.
Infra sync completed.

※開発時は同期してると楽なので、ここでは同期コマンド「sam sync」を利用しています。

準備が整ったので、ここから本格的に開発になります。
まず、ログが出力できるように修正します

/oumu/oumu/app.rb
# require 'httparty'
require 'json'
require 'logger'

def logger
  @logger ||= Logger.new($stdout, level: Logger::Severity::DEBUG)
end

def lambda_handler(event:, context:)
  logger.debug(event)
  logger.debug(context)

  { statusCode: 200 }
end

line works botからlambdaにpostが届いているか確認します
image.png

cloudWatchで確認すると、ログが出力されていることが分かります
image.png

line works bot api

oumu/oumu/Gemfile
source 'https://rubygems.org'

gem 'httparty'

ruby '~> 3.2.0'

gem 'jwt'

line works bot の api を別ファイルで作成します。
CLIENT_ID、CLIENT_SECRET、SERVICE_ACCOUNT_ID、PRIVATE_KEY、BOT_IDは自分が発行したものに変更してから利用してください。

oumu/oumu/line_works_send.rb
require 'jwt'

CLIENT_ID = 'hogehoge'
CLIENT_SECRET = 'hogehoge'
SERVICE_ACCOUNT_ID = 'hogehoge'
PRIVATE_KEY = "hogehoge"
SCOPE = 'bot'
BOT_ID = '1'

def jwt
  current_time = Time.now.to_i
  rsa_private = OpenSSL::PKey::RSA.new(PRIVATE_KEY)
  payload = { iss: CLIENT_ID, sub: SERVICE_ACCOUNT_ID, iat: current_time, exp: current_time + 3600 }
  JWT.encode(payload, rsa_private, 'RS256') # 暗号化
end

def create_access_token
  url = 'https://auth.worksmobile.com/oauth2/v2.0/token'
  headers = { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }
  params = {
    assertion: jwt, grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
    client_id: CLIENT_ID, client_secret: CLIENT_SECRET, scope: SCOPE
  }
  response = Net::HTTP.post(URI.parse(url), URI.encode_www_form(params), headers)
  JSON.parse(response.body)
end

def lw_api_headers_params
  access_token = create_access_token
  {
    'Content-type': 'application/json',
    Authorization: "#{access_token['token_type']} #{access_token['access_token']}"
  }
end

def lw_api_message_params(message)
  {
    content: {
      type: 'text',
      text: message.to_s
    }
  }
end

def bot_send_message(user_id, message)
  url = "https://www.worksapis.com/v1.0/bots/#{BOT_ID}/users/#{user_id}/messages"
  params = lw_api_message_params(message)
  response = Net::HTTP.post(URI.parse(url), params.to_json, lw_api_headers_params)
  response.code
end

上記で作ったものを呼び出し、lambda_handlerに設定します

oumu/oumu/app.rb
# require 'httparty'
require 'json'
require 'logger'
require './line_works_send'

def logger
  @logger ||= Logger.new($stdout, level: Logger::Severity::DEBUG)
end

def lambda_handler(event:, context:)
  line_post = JSON.parse(event['body'])
  logger.debug(line_post)
  user_id = line_post.dig('source', 'userId')
  logger.debug(user_id)
  message = line_post.dig('content', 'text')
  logger.debug(message)
  s_code = bot_send_message(user_id, message)
  logger.debug(s_code)
  { statusCode: s_code }
end

動作検証

image.png

Bot に話しかけると、オウム返しされます。

署名検証

公式ページを見ると、セキュリティの観点から署名検証をするように記載がありますので、追加します

image.png

公式に記載されているBot Secretと比較するために、lambdaに渡す必要があるので、テンプレートを修正します

oumu/template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  oumu

  Sample SAM Template for oumu

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3
    MemorySize: 128

+ Parameters:
+   LineWorksBotSecret:
+     Type: String
+     Description: Line Works Bot Secret

Resources:
  OumuFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: oumu/
      Handler: app.lambda_handler
      Runtime: ruby3.2
+     Environment:
+       Variables:
+         LINE_WORKS_BOT_SECRET: !Ref LineWorksBotSecret
      Architectures:
        - x86_64
      Events:
        OumuApi:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /oumu
            Method: post

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  OumuApi:
    Description: "API Gateway endpoint URL for Prod stage for oumu function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/oumu/"
  OumuFunction:
    Description: "oumu Lambda Function ARN"
    Value: !GetAtt OumuFunction.Arn
  OumuFunctionIamRole:
    Description: "Implicit IAM Role created for oumu function"
    Value: !GetAtt OumuFunctionRole.Arn

反映させます

$ sam build
$ sam deploy --guided

すると、デプロイ時にパラメータを聞かれますので、bot画面のbot secretを入力します
image.png

image.png

oumu/oumu/app.rb
# require 'httparty'
require 'json'
require 'logger'
require './line_works_send'
# require 'rack'
require 'openssl'
require 'base64'

def logger
  @logger ||= Logger.new($stdout, level: Logger::Severity::DEBUG)
end

+ def verify_signature(event)
+   hmac = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), ENV.fetch('LINE_WORKS_BOT_SECRET'), event['body'])
+   hash2 = Base64.strict_encode64(hmac)
+   hash2 != event.dig('headers', 'x-works-signature')
+ end

def lambda_handler(event:, context:)
+   return { statusCode: 200 } if verify_signature(event) # 違う場合は、何もせずに200を返す

  request_body = JSON.parse(event['body'])
  user_id = request_body.dig('source', 'userId')
  message = request_body.dig('content', 'text')
  s_code = user_send_message(user_id, message)
  { statusCode: s_code }
end

これで完成となります

再デプロイを行いますのでエンドポイントも変わります。bot側の変更も必要なのでご注意ください

さいごに

今回は簡単なオウムbotを作成しました。
lambda側でopenAIなどと繋げば色々な応用が作れそうとは思っています。
が、もう少し学習が必要だと感じていますので、少しずつ何かを作っていこうと考えております。
拙いですが、最後までご拝読頂けますと幸いです。

2
0
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
2
0