この記事はぷりぷりあぷりけーしょんず Advent Calendar 2019の23日目の記事です。
はじめに
好きなバンドとかアーティストを共有したいときってありませんか?わたしはあります。
そういったときにSlack上でサクッと検索ができたら幸せだなあと思い、アーティスト名を入力すると、そのアーティストの人気曲Top10を返すSlack botを作りました。
Infrastructure as Code の学習も兼ねて、Lambda + API Gateway はCDKで定義しています。
使用技術
Slack Outgoing Webhook
Spotify API
AWS Lambda
Amazon API Gateway
AWS CDK 1.19.0
Node.js 12.8.1
※ CDK, Node の環境構築は完了していることを前提とします。
アーキテクチャ
アーキテクチャはこのようなイメージです。
ざっくり説明すると、SlackのOutgoing WebhookからAPI GatewayにPOSTし、受け取った文字列をもとにSpotify APIに検索をかけて、その結果をSlackに返すようになっています。
ディレクトリ構成
CDKでプロジェクトを作成するので、ディレクトリ構成はこのようになります。
├── bin
│ └── 〇〇-cdk.ts
├── lambda
│ ├── node_modeules
│ ├── package.json
│ ├── package-lock.json
│ └── search.js
├── lib
│ └── 〇〇-stack.ts
├── node_modeules
├── README.md
├── package-lock.json
├── package.json
└── tsconfig.json
Spotify APIを使えるようにする
Spotify APIを使用するためには、クライアントアプリケーションを作成して認証を通す必要があります。
下記URLからダッシュボードにサインアップ / ログインすることができます。
https://developer.spotify.com/dashboard/
ダッシュボードにログインができたら、CREATE AN APP
をクリックしてクライアントアプリを作成します。
アプリが作成されたら、Client Id
とClient Secret
をメモしておきます。
Spotify APIには3種類の認証方法がありますが、今回はクライアント側から直接APIを叩く構成ではないため、Client Credentials Flow
という認証フローを採用しています。
各認証方法についての詳細はこちらをご参照ください。
https://developer.spotify.com/documentation/general/guides/authorization-guide/
CDKでAWSリソースを定義していく
AWSリソースと、Lambdaにデプロイするコードを書いていきます。
Nodeのプログラムを作る
/lambda
ディレクトリを作成し、そのディレクトリでnpm init
でpackage.jsonを作成したら、
npm install request
で request のパッケージをインストールしておきます。
exports.handler=(event,context,callback)=>{constrequest=require('request');constauthOptions={url:'https://accounts.spotify.com/api/token',headers:{'Authorization':'Basic '+process.env.ACCESS_TOKEN},form:{grant_type:'client_credentials'},json:true};request.post(authOptions,function(error,response,body){if(error){console.log('POST Error: '+error.message);return;}consttoken=body.access_token;constartist=event.text.slice(5);constencoded=encodeURIComponent(artist);constoptions={url:'https://api.spotify.com/v1/search?q='+encoded+'&type=artist&market=JP&limit=1&offset=0',headers:{'Authorization':'Bearer '+token},json:true};request.get(options,function(error,response,body){letres={};if(error){console.log('GET Error: '+error.message);res.text='検索に失敗しました。ごめんなさい!';callback(null,res);}else{res.text=body.artists.items[0].external_urls.spotify;callback(null,res);}});});};
SlackからPOSTされた文字列はevent.text
で取得することができます。Outgoing Webhookのトリガーとなる文字列を除くためslice(5)
としています。
あとはその文字列(アーティスト名)をエンコードして、https://api.spotify.com/v1/search
のクエリパラメータに含めてリクエストをするだけです。
APIの細かい仕様はこちらをご参照ください。
https://developer.spotify.com/documentation/web-api/reference/search/search/
レスポンスが複数の場合もありえますが、今回は1件目だけをSlackに返すようにしています。
Lambdaを定義する
npm install @aws-cdk/aws-lambda
でLambdaのライブラリをインストールしたら、/lib
配下のtsファイルにコードを書いていきます。
importcdk=require('@aws-cdk/core');importlambda=require('@aws-cdk/aws-lambda');import{Duration}from'@aws-cdk/core';exportclassPurivisualSearchCdkStackextendscdk.Stack{constructor(scope:cdk.Construct,id:string,props?:cdk.StackProps){super(scope,id,props);// lambdaconstsearch=newlambda.Function(this,'SearchHandler',{runtime:lambda.Runtime.NODEJS_10_X,code:lambda.Code.fromAsset('lambda'),handler:'search.handler',timeout:Duration.minutes(1),environment:{"ACCESS_TOKEN":"< your access token >"}});}}
environment
の< your access token >
には、Spotifyでアプリを作成したときに発行したClient Id
とClient Secret
をbase64でエンコードした文字列を入れてください。
ターミナルで下記コマンド叩くとエンコードした文字列を出力できます。
echo -n < Client Id >:< Client Secret > | base64
API Gatewayを定義する
npm install @aws-cdk/aws-apigateway
でAPI Gatewayのライブラリをインストールします。
importcdk=require('@aws-cdk/core');importlambda=require('@aws-cdk/aws-lambda');importapigw=require('@aws-cdk/aws-apigateway');import{Duration}from'@aws-cdk/core';exportclassPurivisualSearchCdkStackextendscdk.Stack{constructor(scope:cdk.Construct,id:string,props?:cdk.StackProps){super(scope,id,props);// lambdaconstsearch=newlambda.Function(this,'SearchHandler',{runtime:lambda.Runtime.NODEJS_10_X,code:lambda.Code.fromAsset('lambda'),handler:'search.handler',timeout:Duration.minutes(1),environment:{"ACCESS_TOKEN":"< your accsess token >"}});// api gatewayconstapi=newapigw.LambdaRestApi(this,'PurivisualSearchApi',{handler:search,proxy:false});// リソースの作成constpostResouse=api.root.addResource("serach")constresponseModel=api.addModel('ResponseModel',{contentType:'application/json',modelName:'ResponseModel',schema:{}});consttemplate:string='## convert HTML POST data or HTTP GET query string to JSON\n'+'\n'+'## get the raw post data from the AWS built-in variable and give it a nicer name\n'+'#if ($context.httpMethod == "POST")\n'+' #set($rawAPIData = $input.path(\'$\'))\n'+'#elseif ($context.httpMethod == "GET")\n'+' #set($rawAPIData = $input.params().querystring)\n'+' #set($rawAPIData = $rawAPIData.toString())\n'+' #set($rawAPIDataLength = $rawAPIData.length() - 1)\n'+' #set($rawAPIData = $rawAPIData.substring(1, $rawAPIDataLength))\n'+' #set($rawAPIData = $rawAPIData.replace(", ", "&"))\n'+'#else\n'+' #set($rawAPIData = "")\n'+'#end\n'+'\n'+'## first we get the number of "&" in the string, this tells us if there is more than one key value pair\n'+'#set($countAmpersands = $rawAPIData.length() - $rawAPIData.replace("&", "").length())\n'+'\n'+'## if there are no "&" at all then we have only one key value pair.\n'+'## we append an ampersand to the string so that we can tokenise it the same way as multiple kv pairs.\n'+'## the "empty" kv pair to the right of the ampersand will be ignored anyway.\n'+'#if ($countAmpersands == 0)\n'+' #set($rawPostData = $rawAPIData + "&")\n'+'#end\n'+'\n'+'## now we tokenise using the ampersand(s)\n'+'#set($tokenisedAmpersand = $rawAPIData.split("&"))\n'+'\n'+'## we set up a variable to hold the valid key value pairs\n'+'#set($tokenisedEquals = [])\n'+'\n'+'## now we set up a loop to find the valid key value pairs, which must contain only one "="\n'+'#foreach( $kvPair in $tokenisedAmpersand )\n'+' #set($countEquals = $kvPair.length() - $kvPair.replace("=", "").length())\n'+' #if ($countEquals == 1)\n'+' #set($kvTokenised = $kvPair.split("="))\n'+' #if ($kvTokenised[0].length() > 0)\n'+' ## we found a valid key value pair. add it to the list.\n'+' #set($devNull = $tokenisedEquals.add($kvPair))\n'+' #end\n'+' #end\n'+'#end\n'+'\n'+'## next we set up our loop inside the output structure "{" and "}"\n'+'{\n'+'#foreach( $kvPair in $tokenisedEquals )\n'+' ## finally we output the JSON for this pair and append a comma if this isn\'t the last pair\n'+' #set($kvTokenised = $kvPair.split("="))\n'+' "$util.urlDecode($kvTokenised[0])" : #if($kvTokenised[1].length() > 0)"$util.urlDecode($kvTokenised[1])"#{else}""#end#if( $foreach.hasNext ),#end\n'+'#end\n'+'}'// POSTメソッドの作成postResouse.addMethod("POST",newapigw.LambdaIntegration(search,{// 統合リクエストの設定requestTemplates:{'application/x-www-form-urlencoded':template},// 統合レスポンスの設定integrationResponses:[{statusCode:'200',contentHandling:apigw.ContentHandling.CONVERT_TO_TEXT,responseTemplates:{'application/json':"$input.json('$')"}}],passthroughBehavior:apigw.PassthroughBehavior.WHEN_NO_MATCH,proxy:false}),// メソッドレスポンスの設定{methodResponses:[{statusCode:'200',responseModels:{'application/json':responseModel}}]})}}
apigw.LambdaRestApi()
のhandlerに先ほど定義したLambdaを指定してあげることで、LambdaをバックエンドとしたAPIを作成することができます。
SlackからPOSTされるデータはapplication/x-www-form-urlencoded
形式のため、jsonに変換しています。AWSフォーラムで紹介されているマッピングテンプレートをまるっとコピーして使用しています。
https://forums.aws.amazon.com/thread.jspa?messageID=673012&tstart=0#673012
デプロイ
これでAWSリソースとLambdaにデプロイするプログラムが完成したので、デプロイします。
cdk diff
cdk deploy
SlackのOutgoing Webhookを設定する
「引き金となる言葉」には被ることがないような文言を設定しておくのが無難です。わたしは推しの名前にしました。
あとは、URLに作成したAPI Gatewayのエンドポイントを指定し、名前やアイコンなどを設定して保存すればbotの完成です。
このように、「SORA」のあとにスペース+アーティスト名で、そのアーティストの人気曲Top10が表示されるようになりました!残念ながらSlack上で再生ができるは視聴版のみとなっており、全曲フル尺で再生したい場合はリンクからSpotifyを開く必要があります。
最後に
こういうbotがあれば「このアーティストオススメだから聴いてみて!」が簡単にできて楽しいかなと思って作ってみました。
あとbotのアイコンを推しにするとかなり愛着が湧きます!
Spotify APIは他にも色んなエンドポイントが用意されているので、気になる方は是非使ってみてください!