AWS LambdaでTwitterギャル文字変換BOTを作った

  • 6
    いいね
  • 0
    コメント

前回Webページのギャル文字化に成功したので、次はTwitterに挑んでみることにした
言語は今回もScalaで(Twitterだけに)
副作用があるラムダってなんか違和感があるが細かいことは気にしない

今回の仕様

  • Twitterの自分宛のメンションをひたすらギャル文字に変換して返信する

※当初はハッシュタグでやろうと思ったが、Twitterルールのスパム項にある、

ハッシュタグ、トレンドトピックや人気のトピック、プロモトレンドなどを使用し、そのトピックとは関係のない更新を複数投稿した場合

に抵触しそうな気がしたのでメンションにした(十分グレーな気もするが)

  • Twitterにはトリガーっぽいのが見当たらなかったため、定期的にラムダから監視する

構成図

image

作った手順

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を作った

twitter_gal_moji
{
  id: "last_id"
  value: 0
}

5. AWS IAMロールの編集

前回まで使ってたラムダ実行用のロールにDynamoDBへのアクセス許可AmazonDynamoDBFullAccessを与える
これをしないとLambdaからアクセスしたときにエラーになる
image

6. DynamoDBへの読み込みAPI、及び書き込みAPIの作成

ScalaからDynamoDBへのアクセスが面倒そうだったのでNodeJSのラムダとAPI Gatewayで作った
(Nodeが非常に簡単すぎた)

読み込み

get-twitter-gal-moji-last-id
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)
        }
    });
};

image

書き込み

読み込みを/twitter-gal-moji-last-idリソースのGETメソッドで公開したので、
本来ならばREST的にどう考えても同じ/twitter-gal-moji-last-idPUTメソッドとすべきであるが、HTTPSのハンドシェイクエラーでどはまりしたので泣く泣くGETで公開した
PUTで公開した(ライブラリの問題だったっぽい。。)

put-twitter-gal-moji-last-id
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);
        }
    });
};

image

更新値はパスパラメータにしたので
統合リクエストの本文マッピングテンプレートを

image
こうする

また、更新系なので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
  }
GalMoji.scala
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
image
課金とTwitterのアカウントBANが怖いので5分置きに起動することにした

実行結果

image

約5分後。。

image

できた!

Twitterの@galmoji はしばらく生かしとくのでよかったら試してください

今回のソース

後日公開予定

所感

他サービスとの連携、特に副作用を伴う場合はテストに気をつかう
次は真面目なものを作ろう。。