個人で使っている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へ通知する。
実装
実装には Chalice を使った。Chaliceはいわゆるサーバーレスアプリケーションを実現するためのフレームワークで、簡単にLambda + API Gatewayを活用したアプリケーションをデプロイできる。
本来であれば複数のメソッドを実装し、いわゆるRESTfulなアプリケーションを実現できるわけだが、今回はWebhookで叩くエンドポイントが欲しいだけなので、メソッドは1つのみ。
GitHub上で公開したので、README.mdに従って利用すれば誰でも使えるはず。→ chroju/esa_then_qiita: esa.io posts cross post to Qiita
# -*- 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機能も使える。
- chalichelibというディレクトリに入れたファイルはすべてパッケージングしてデプロイしてくれる。大規模なPythonアプリケーションだったり、変数を外に出しておきたい場合だったり、様々な用途に活用できて幅が広がる。
- 難点としてpython2.7であること。Lambdaが3.x非対応である以上、仕方なしか。
応用
esa.ioを複数人で使用している場合
ここに記載したのはあくまで自分の「個人esa.io」用なので、QiitaのAPIキーも1つしか埋め込んでいない。複数人でesa.ioを使っている場合(その方が多数派ですよね)は、APIキーをdictで持たせておけばよいかと。
{
"QIITA_API_KEY": {
"foo": "295cdXXXXXXXXXXXXX",
"bar": "b21acXXXXXXXXXXXXX",
"baz": "8c1d3XXXXXXXXXXXXX"
},
"SLACK_HOOK_URL": "https://hooks.slack.com/services/xxx",
"SLACK_CHANNEL": "channel"
}
...
headers = {
"Authorization": u"Bearer " + API_KEY[username],
"Content-Type": u"application/json",
"Accept": u"application/json"
}
...
他サービスへの連携
もちろんQiitaだけではなく、同じ形で他のサービスへの投稿もhookできるはず。とりあえず自分の使用範囲でgithub.ioへのブログ投稿も自動化したいのだが、hugoを使っているのでCircleCI上でhugo
コマンドを使うとか、一手間要りそう。