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

[前回](http://qiita.com/bakenezumi/items/5b8dbfabd57c0256ff07#_reference-7de879cf6491f6ae0d9c)Webページのギャル文字化に成功したので、次はTwitterに挑んでみることにした
言語は今回もScalaで（Twitterだけに）
副作用があるラムダってなんか違和感があるが細かいことは気にしない

#今回の仕様
- Twitterの自分宛のメンションをひたすらギャル文字に変換して返信する

※当初はハッシュタグでやろうと思ったが、[Twitterルール](https://support.twitter.com/groups/33-report-a-violation/topics/121-guidelines-best-practices/articles/253501)のスパム項にある、
>ハッシュタグ、トレンドトピックや人気のトピック、プロモトレンドなどを使用し、そのトピックとは関係のない更新を複数投稿した場合

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

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

#構成図
![image](https://qiita-image-store.s3.amazonaws.com/0/35910/a5497115-90c1-99a6-fafb-aae3cb817103.png)

#作った手順
## 1. Twitterアカウントの作成
自分のアカウントがひたすらギャル文字をつぶやくのもいやなので新たにアカウントを作成した

## 2. Twitter Appの登録
[Syncerブログのまとめ記事](https://syncer.jp/twitter-api-matome)を参考に、Twitter Appを登録し、アプリからTwitterにアクセスするために必要なAPI Keyその他もろもろを取得した
必要な情報は下記の４つ

- Consumer Key (API Key)
- Consumer Secret (API Secret)
- Access Token
- Access Token Secret
 
## 3.メンション取得処理の実装(twitter4j)
`getMentions(sinceId: Long)`

twitterとのやりとりは[twitter4j](http://twitter4j.org/ja/index.html)を使った
便利
ここでさっきの４つの情報が必要

```scala
  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(ツイート本文)

の４値タプルのリストを返してるのが`getMentions`

## 4. AWS DynamoDBに最終処理IDの保管場所を作成
1レコード、１項目しか保持しないでよいため非常に単純なテーブル`twitter_gal_moji`を作った

```json:twitter_gal_moji
{
  id: "last_id"
  value: 0
}
```

## 5. AWS IAMロールの編集
前回まで使ってたラムダ実行用のロールにDynamoDBへのアクセス許可`AmazonDynamoDBFullAccess`を与える
これをしないとLambdaからアクセスしたときにエラーになる
![image](https://qiita-image-store.s3.amazonaws.com/0/35910/f9766559-2609-21ee-17d7-a07e0f6fd651.png)

## 6. DynamoDBへの読み込みAPI、及び書き込みAPIの作成
ScalaからDynamoDBへのアクセスが面倒そうだったのでNodeJSのラムダとAPI Gatewayで作った
（Nodeが非常に簡単すぎた）

### 読み込み

```js: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
    };
    // １件しか登録されてない前提なのでscanでとれた1件目のvalue
    dynamo.scan(params, function(err, data) {
        if (err) {
            callback(null, err);
        } else {
            callback(null, data.Items[0].value.N)
        }
    });
};
```
![image](https://qiita-image-store.s3.amazonaws.com/0/35910/d2250282-e8c1-54b3-f684-659e9664970a.png)

### 書き込み

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

```js: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](https://qiita-image-store.s3.amazonaws.com/0/35910/ef63b71b-a75d-72d3-80b9-7548c8d27f07.png)

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

![image](https://qiita-image-store.s3.amazonaws.com/0/35910/15ce21a2-a6dc-505f-b29b-1a31cdf42ff9.png)
こうする

また、更新系なので[toshihirockさんの記事](http://qiita.com/toshihirock/items/8720118164a02dfdd11a)を参考にAPIキー認証を付けた

これで`https://xxxx/prod/twitter-gal-moji-last-id/{id}`の{id}を更新値にして`PUT`すれば更新できる

## 7. Twitterの自分宛のメンションをひたすらギャル文字に変換して返信するBotの作成
`galMojiTweetBot`

```scala
  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
  }
```

```scala:GalMoji.scala
object GalMoji {

  val galMap = Map(
    ('あ' -> "ぁ"), ('い' -> "ﾚヽ"), ('う' -> "ぅ"), ('え' -> "ぇ"), ('お' -> "ぉ"),
    ...
  )

  def toGal(src: String) = src.flatMap(c =>  galMap.getOrElse(c, c.toString))
}
```

`galMojiTweetBot`を解説すると

- 前回処理の最終IDをストレージから取ってくる
- 最終ID以降のメンションを[3.メンション取得処理](#3%E3%83%A1%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%B3%E5%8F%96%E5%BE%97%E5%87%A6%E7%90%86%E3%81%AE%E5%AE%9F%E8%A3%85twitter4j)の`getMentions`関数で取得
- 取れた全てのツイートに対し、`toGalTweet`でギャル語変換してTwitterにTweet
- 今回最終IDをストレージに保管（HTTP PUT には[scalaj-http](https://github.com/scalaj/scalaj-http)を使った）

この`galMojiTweetBot`をハンドラーにしてラムダ登録

トリガーはCloudWatch Events - Schedule
![image](https://qiita-image-store.s3.amazonaws.com/0/35910/34f53d10-725c-f392-8509-b58837f2e3a3.png)
課金とTwitterのアカウントBANが怖いので５分置きに起動することにした

#実行結果
![image](https://qiita-image-store.s3.amazonaws.com/0/35910/21823d39-dbba-8b91-0742-f5bb2d95cfe1.png)

約5分後。。

![image](https://qiita-image-store.s3.amazonaws.com/0/35910/d5fc9674-0c8d-ae68-9d7b-6cb8b923ac1e.png)

**できた！**

#今回のソース
後日公開予定

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