所定のGoogle Driveフォルダにhtmlコンテンツを置いたら半自動でメルマガ配信

メルマガ配信の仕組みを刷新した話
今迄はyamlファイルに必要な情報を記入し、それを元にAWS SQSに送信リストを作り定刻になったらAWS SESで送るって仕組みであった。
テスト送信と本送信のAWS SQSのキューが同じだったので誤送信をしたり、定刻に送信するのをhubot上でsetTimeoutを使って送るというやっつけ仕様であったためheroku dynoが再起動すると送れなかったり、休日もその日に送信時刻を仕込まないと駄目だったりと色々と難があった。

そこで、heroku上にpostgresqlで予約テーブルを作りGoogle Drive上の所定フォルダにhtmlコンテンツを置くとテスト送信まで自動で実施->確認を繰り返し、OKとなったらsubjectと送信日時を入れて予約をする仕組みをslack上で完結できるようにした。

以下は主にhubotで動かしているcoffeesriptサンプル

### 先ずheroku上のpostgresqlに予約テーブルを準備

* sequelizeを使って以下の様なものを準備  
`htmlコンテンツのファイル名` `送信日時` `subject` `状態` などなど
* stateには `initialize` `review` `verified` `preparing` `available` `completed` などを持たせて状態管理が出来るようにしておいた  
本送信が出来るのは `available` の状態のときのもののとかにする  
本当は[machina](https://www.npmjs.com/package/machina)を使ってみたかった

```coffeescript
Sequelize    = require('sequelize')
db = new Sequelize(process.env.DATABASE_URL)
EmagazineSchedule = db.define('EmagazineSchedule', {
  send_at:    {type: Sequelize.DATE},
  file_id:    {type: Sequelize.STRING, allowNull: false},
  filename:   {type: Sequelize.STRING, allowNull: false, unique: true},
  subject:    {type: Sequelize.TEXT},
  state:      {type: Sequelize.STRING,  defaultValue: 'initialize'},
}, {
  paranoid:       true,
  underscored:    true,
  underscoredAll: true,
  deletedAt:      'deleted_at',
  tableName:      'emagazine_schedules',
})
```

### Google Driveの所定フォルダにhtmlファイルを置いたら予約テーブルにレコードが追加されるようにする

* 所定のフォルダに変更がかかった場合、hubotに通知が来るようにする  
Google Driveの設定は[こちら](http://qiita.com/hotakasaito/items/824b7612667e32328345)を参照
* Googleからの通知が受けれるようにhubot上にendpointを作る  
https://***.herokuapp.com/google/drive/notifications/emagazine で受ける

```coffeescript
Googleapis   = require('googleapis')
drive        = Googleapis.drive({version: 'v2'})
SERVICE_ACCOUNT_EMAIL = 'xxx@developer.gserviceaccount.com'
SERVICE_ACCOUNT_KEY   = process.env.EMAGAZINE_PEM
SCOPE = [
  "https://www.googleapis.com/auth/drive",
  "https://www.googleapis.com/auth/drive.appdata",
  "https://www.googleapis.com/auth/drive.apps.readonly",
  "https://www.googleapis.com/auth/drive.file",
  "https://www.googleapis.com/auth/drive.metadata.readonly",
  "https://www.googleapis.com/auth/drive.readonly"
]
jwt = new Googleapis.auth.JWT(SERVICE_ACCOUNT_EMAIL, null, SERVICE_ACCOUNT_KEY, SCOPE)
jwt.authorize (err, tokens) ->
  if err
    console.log err
  else
    jwt.credentials = tokens

module.exports = (robot) ->
  robot.router.post "/google/drive/notifications/emagazine", (req, res) ->
    drive.changes.get {auth: jwt, changeId: data.body.id}, (err, resp) ->
      if (not resp.file.labels.trashed and resp.file.mimeType == 'text/html')
        filename = resp.file.title
        db.transaction().then (t) ->
          EmagazineSchedule.findOrCreate({where: {file_id: resp.file.id, filename: filename}, paranoid: false}, {transaction: t, lock: t.LOCK.UPDATE}).spread (schedule, created) ->
            # ここにテスト用の送信リスト作成と送信を実施するコードを入れる
            ...
          .catch (err) ->
            t.rollback()
```

### 送信リスト(AWS SQSにemailアドレスリストを作る)と送信はコマンドを準備

* これは送信サーバ(バッチサーバ)にコマンドを準備した
* hubotからはsshでこれらのコマンドを呼び出してSTDOUTとSTDERRを受け取って処理を実施するのみ
* バッチサーバからDrive上のコンテンツを取得するのは[google-drive-ocamlfuse](https://github.com/astrada/google-drive-ocamlfuse)を使ってmountして使用  
`metadata_cache_time=60` で設定しているのでテストで再送信する場合は60秒の待ちを作ったりする必要あり

### テスト送信

* 送信リストの作成と送信を実施
* テスト送信用のアドレスリストを予め準備しておいた  
これもhubotにendpointを作り、どこからでもhttp getで取得出来るようにしておいた

### 本送信用のリスト作成と本送信

* agendaを使って定期的に予約テーブルを確認して該当するものがあれば実行するようにした
* 送信時刻が判明しているのでしっかりとスケジューリングが出来るのでcronの様に毎分確認とする仕様でなくても良いが、TODOとしている
* EventEmitterでタスクを呼び出すようにしている
* 以下は送信リストを作る箇所のサンプル

#### agendaで定期的にイベントを呼び出す処理

```coffeescript
Agenda       = require('agenda')
agenda       = new Agenda({db: { address: process.env.MONGOLAB_URI}})
module.exports = (robot) ->
  agenda.define 'agenda:emagazine:preparing', (job, done) ->
    robot.logger.info '@agenda:emagazine:preparing'
    robot.emit 'emagazine:preparing', {execute: true}
    done()

  agenda.every('5 minutes', 'agenda:emagazine:preparing')
  agenda.start()
```

#### 実際に送信リストを作る処理

* トランザクション分離レベルは状況に合わせて変更  

```coffeescript
module.exports = (robot) ->
  robot.on 'emagazine:preparing', (params) ->
    db.transaction({isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE}).then (t) ->
      ...
```

### slackからの操作

以下の処理が行えるようにした

* 予約テーブル一覧表示
* テスト再送信
* 送信日時/subjectの設定
* 承認
* 予約の取り消し

```coffeescript
module.exports = (robot) ->
  robot.respond /\bschedule\s+emagazine\s+(show|resend|update|verify|destroy|...)\s+(.+)$/, (msg) ->
    ...
```

---
現在テスト運用中で運用上改修したことなどは今後追記予定