esa.ioで書いた記事をQiitaへマルチポストする

個人で使っているesa.ioでブログやQiitaの下書きをしているのだが、その後アップロードするときに「手でコピペ」しているのが面倒になり、Webhookを使った自動マルチポストを実装してみた。

要件
----

* esa.ioで新しく「#qiita」タグの付いた記事を作成したとき、Webhookでesa.io記事作成者（screen name）と同名のユーザーでQiitaへ同じ記事を投稿する。
* esa.io上ではディレクトリ構成を含んだ「path/to/article title」というタイトル形式を取るが、アップロード後はディレクトリ部分を除いた「article title」のタイトルにする。
* タイトルが同じ記事がすでにQiitaにある場合は投稿しない。
* esa.ioで設定していたタグをQiitaの記事にも同様に設定する。
* 投稿後にQiita記事のURLをSlackへ通知する。

![slack image](https://qiita-image-store.s3.amazonaws.com/0/19465/3afd6b37-52ea-05d7-03ea-761f78fbfbd5.png)

実装
----

実装には [Chalice](https://github.com/awslabs/chalice) を使った。Chaliceはいわゆるサーバーレスアプリケーションを実現するためのフレームワークで、簡単にLambda + API Gatewayを活用したアプリケーションをデプロイできる。

本来であれば複数のメソッドを実装し、いわゆるRESTfulなアプリケーションを実現できるわけだが、今回はWebhookで叩くエンドポイントが欲しいだけなので、メソッドは1つのみ。

GitHub上で公開したので、README.mdに従って利用すれば誰でも使えるはず。→ [chroju/esa_then_qiita: esa.io posts cross post to Qiita](https://github.com/chroju/esa_then_qiita)

```python:app.py
# -*- coding: utf-8 -*-
import json
import os
from chalice import Chalice
import requests

app = Chalice(app_name='esa_then_qiita')
app.debug = True
API_URL = u"http://qiita.com/api/v2/"

filename = os.path.join(os.path.dirname(__file__), "chalicelib", "config.json")
with open(filename) as f:
    config = json.load(f)
API_KEY = config["QIITA_API_KEY"]
SLACK_HOOK_URL = config["SLACK_HOOK_URL"]
SLACK_CHANNEL = config["SLACK_CHANNEL"]

@app.route('/qiita', methods=['POST'])
def index():
    # parse request
    request = app.current_request.json_body
    raw_title = request["post"]["name"].split(u"/")[-1]
    username = request["user"]["screen_name"]
    title_and_tags = [ i.strip() for i in raw_title.split("#") ]

    # find qiita tag
    if u"qiita" not in title_and_tags[1:]:
        return u"nothing to do (this post is not for qiita)"

    # check articles duplication
    past_items = requests.get(url=API_URL + 'items?page=1&per_page=20&query=user%3A' + username)
    past_titles = [ item["title"] for item in past_items.json() ]
    while "next" in past_items.links:
        past_items = requests.get(past_items.links["next"]["url"])
        past_titles.extend([ item["title"] for item in past_items.json() ])
    if title_and_tags[0] in past_titles:
        return u"nothing to do (same title post already exists)"

    # set up qiita request
    qiita_input_dict = {
        "title": title_and_tags[0],
        "body": request["post"]["body_md"],
        "gist": False,
        "private": False,
        "tweet": False,
        "tags": [ {"name": tag} for tag in title_and_tags[1:] if tag.find(u"qiita") == -1 ]
    }
    headers = {
        "Authorization": u"Bearer " + API_KEY,
        "Content-Type": u"application/json",
        "Accept": u"application/json"
    }

    # post qiita
    r = requests.post(url=API_URL + "items", data=json.dumps(qiita_input_dict), headers=headers)

    # post result to slack
    slack_input_dict = {
        "text": u"esa.io -> qiita done.\n{}".format(r.json()["url"]),
        "channel": SLACK_CHANNEL
    }
    if r.status_code == 201 and SLACK_HOOK_URL != "":
        requests.post(url=SLACK_HOOK_URL, data=json.dumps(slack_input_dict))
    return r.text
```

chaliceについて
----

chaliceは今回初めてきちんと使ったけど、API Gateway + Lambdaを扱う上では非常に楽な選択肢の1つだと感じた。

* local実行モードがあるので、debugが楽。
* debugにあたっては[logging](http://chalice.readthedocs.io/en/latest/topics/logging.html)機能も使える。
* [chalichelib](http://chalice.readthedocs.io/en/latest/topics/multifile.html)というディレクトリに入れたファイルはすべてパッケージングしてデプロイしてくれる。大規模なPythonアプリケーションだったり、変数を外に出しておきたい場合だったり、様々な用途に活用できて幅が広がる。
* 難点としてpython2.7であること。Lambdaが3.x非対応である以上、仕方なしか。

応用
----

### esa.ioを複数人で使用している場合

ここに記載したのはあくまで自分の「個人esa.io」用なので、QiitaのAPIキーも1つしか埋め込んでいない。複数人でesa.ioを使っている場合（その方が多数派ですよね）は、APIキーをdictで持たせておけばよいかと。

```json:config.json
{
  "QIITA_API_KEY": {
    "foo": "295cdXXXXXXXXXXXXX",
    "bar": "b21acXXXXXXXXXXXXX",
    "baz": "8c1d3XXXXXXXXXXXXX"
  },
  "SLACK_HOOK_URL": "https://hooks.slack.com/services/xxx",
  "SLACK_CHANNEL": "channel"
}
```

```python:app.py
...
    headers = {
        "Authorization": u"Bearer " + API_KEY[username],
        "Content-Type": u"application/json",
        "Accept": u"application/json"
    }
...
```

### 他サービスへの連携

もちろんQiitaだけではなく、同じ形で他のサービスへの投稿もhookできるはず。とりあえず自分の使用範囲でgithub.ioへのブログ投稿も自動化したいのだが、hugoを使っているのでCircleCI上で`hugo`コマンドを使うとか、一手間要りそう。