前回Webページのギャル文字化に成功したので、次はTwitterに挑んでみることにした
言語は今回もScalaで(Twitterだけに)
副作用があるラムダってなんか違和感があるが細かいことは気にしない
今回の仕様
- Twitterの自分宛のメンションをひたすらギャル文字に変換して返信する
※当初はハッシュタグでやろうと思ったが、Twitterルールのスパム項にある、
ハッシュタグ、トレンドトピックや人気のトピック、プロモトレンドなどを使用し、そのトピックとは関係のない更新を複数投稿した場合
に抵触しそうな気がしたのでメンションにした(十分グレーな気もするが)
- Twitterにはトリガーっぽいのが見当たらなかったため、定期的にラムダから監視する
構成図
作った手順
1. Twitterアカウントの作成
自分のアカウントがひたすらギャル文字をつぶやくのもいやなので新たにアカウントを作成した
2. Twitter Appの登録
Syncerブログのまとめ記事を参考に、Twitter Appを登録し、アプリからTwitterにアクセスするために必要なAPI Keyその他もろもろを取得した
必要な情報は下記の4つ
- Consumer Key (API Key)
- Consumer Secret (API Secret)
- Access Token
- Access Token Secret
3.メンション取得処理の実装(twitter4j)
getMentions(sinceId: Long)
twitterとのやりとりはtwitter4jを使った
便利
ここでさっきの4つの情報が必要
lazy val twitter = {
import org.pac4j.oauth.profile.twitter.TwitterProfile
import twitter4j.auth.AccessToken
import twitter4j.TwitterFactory
val apiKey = "XXXXXXXXXXXXXXXXXXX"
val apiSecret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
val accessToken = "999999999-XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
val accessTokenSecret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
val tw = TwitterFactory.getSingleton
tw.setOAuthConsumer(apiKey, apiSecret)
val ac = new AccessToken(accessToken, accessTokenSecret)
tw.setOAuthAccessToken(ac)
tw
}
def getMentions(sinceId: Long) = {
import collection.JavaConversions._
twitter.getMentionsTimeline.filter(t => t.getLang == "ja" && t.getId > sinceId).map(
s => (s.getId, s.getCreatedAt, s.getUser.getScreenName, s.getText)
)
}
API Keyなどは本来はコード上に書くべきでなく、環境変数かパラメータで渡すのがお作法だが、面倒なのでベタ書きした。GitHubあげるときにリファクタする
コードの解説をすると
Twitterから自分宛のメンションタイムラインを取得し、日本語かつ引数で渡されたidより大きいもののみにフィルタして、
- id(インクリメンタル?なロング値。ローテーションしたら機能停止)
- createdAt(ツイートした日時)
- screenName(@XXのやつ)
- text(ツイート本文)
の4値タプルのリストを返してるのがgetMentions
4. AWS DynamoDBに最終処理IDの保管場所を作成
1レコード、1項目しか保持しないでよいため非常に単純なテーブルtwitter_gal_moji
を作った
{
id: "last_id"
value: 0
}
5. AWS IAMロールの編集
前回まで使ってたラムダ実行用のロールにDynamoDBへのアクセス許可AmazonDynamoDBFullAccess
を与える
これをしないとLambdaからアクセスしたときにエラーになる
6. DynamoDBへの読み込みAPI、及び書き込みAPIの作成
ScalaからDynamoDBへのアクセスが面倒そうだったのでNodeJSのラムダとAPI Gatewayで作った
(Nodeが非常に簡単すぎた)
読み込み
var AWS = require('aws-sdk');
var dynamo = new AWS.DynamoDB({
region: 'ap-northeast-1'
});
var tableName = "twitter_gal_moji";
exports.handler = function(event, context, callback) {
var params = {
"TableName": tableName
};
// 1件しか登録されてない前提なのでscanでとれた1件目のvalue
dynamo.scan(params, function(err, data) {
if (err) {
callback(null, err);
} else {
callback(null, data.Items[0].value.N)
}
});
};
書き込み
読み込みを/twitter-gal-moji-last-id
リソースのGET
メソッドで公開したので、
本来ならばREST的にどう考えても同じ/twitter-gal-moji-last-id
のPUT
メソッドとすべきであるが、HTTPSのハンドシェイクエラーでどはまりしたので泣く泣くGET
で公開した
PUT
で公開した(ライブラリの問題だったっぽい。。)
var AWS = require('aws-sdk');
var dynamo = new AWS.DynamoDB({
region: 'ap-northeast-1'
});
var tableName = "twitter_gal_moji";
exports.handler = function(event, context) {
var params = {
TableName: tableName,
Key: {
"id": {"S": "last_id"}
},
AttributeUpdates: {
"value": {
'Action': 'PUT',
'Value': {"N": event.last_id}
}
}
}
dynamo.updateItem(params, function(err, data) {
if (err) {
context.fail(err);
} else {
context.succeed(data);
}
});
};
更新値はパスパラメータにしたので
統合リクエストの本文マッピングテンプレートを
また、更新系なのでtoshihirockさんの記事を参考にAPIキー認証を付けた
これでhttps://xxxx/prod/twitter-gal-moji-last-id/{id}
の{id}を更新値にしてPUT
すれば更新できる
7. Twitterの自分宛のメンションをひたすらギャル文字に変換して返信するBotの作成
galMojiTweetBot
def tweet(message: String) = {
twitter.updateStatus(message)
}
// hogehogeとAPI KEYは内緒
val lastIdURL = "https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/prod/twitter-gal-moji-last-id"
val xApiKey = "XXXXXXXXXXXXXXXXXXXXXXXXXXX"
def getLastId = {
import scala.io.Source
Source.fromURL(lastIdURL).mkString.toLong
}
def putLastId(id: Long) = {
import scalaj.http.Http
Http(lastIdURL + "/" + id).method("PUT")
.header("Content-Type", "application/json")
.header("x-api-key", xApiKey)
.asString.body
}
def toGalTweet(tweet: String, screenName: String) = {
import galmoji._
val myName = "@galmoji"
GalMoji.toGal(tweet.replaceAll(myName, "@" + screenName))
}
def galMojiTweetBot = {
val lastId = getLastId
val newLastId = getMentions(lastId).foldLeft(lastId){ (maxId, t) =>
val (id, createdAt, screenName, text) = t
val reply = toGalTweet(text, screenName)
tweet(reply)
if (id > maxId) id else maxId
}
putLastId(newLastId)
newLastId
}
object GalMoji {
val galMap = Map(
('あ' -> "ぁ"), ('い' -> "レヽ"), ('う' -> "ぅ"), ('え' -> "ぇ"), ('お' -> "ぉ"),
...
)
def toGal(src: String) = src.flatMap(c => galMap.getOrElse(c, c.toString))
}
galMojiTweetBot
を解説すると
- 前回処理の最終IDをストレージから取ってくる
- 最終ID以降のメンションを3.メンション取得処理の
getMentions
関数で取得 - 取れた全てのツイートに対し、
toGalTweet
でギャル語変換してTwitterにTweet - 今回最終IDをストレージに保管(HTTP PUT にはscalaj-httpを使った)
このgalMojiTweetBot
をハンドラーにしてラムダ登録
トリガーはCloudWatch Events - Schedule
課金とTwitterのアカウントBANが怖いので5分置きに起動することにした
実行結果
約5分後。。
できた!
今回のソース
後日公開予定
所感
他サービスとの連携、特に副作用を伴う場合はテストに気をつかう
次は真面目なものを作ろう。。