この記事はAdvent Calendar 2019 41日目の記事です。
本当は別の話をする予定だったんですが内容がなくなりそうだったので急遽変更しました。
一昔前はみんな自分のサーバーにブログ持ってたらしいんですけど最近はあんまりそういう話聞きませんね(はてなとかが充実してるからいらない気もしますが)。というか私はその時代インターネットに触れてなかったのでその話自体知らないんですけど。
ともあれ私の周囲では「みんなブログ持っとる、持ってないのお前だけ」って感じだったので私もVPSを借りてブログを立てました。ブログシステムを自分で作ってる全強とかもいた気がしますが私は全強ではないのでGhost(https://ghost.org/)を利用しました。
投稿ツイートしたい...したくない?
さて、皆さんご覧のこのtraPブログもそのGhostで動いているのですが(これ言っていいのかな)、そこに記事が投稿されると公式Twitterアカウントでその旨がツイートされます。
こんな感じのやつです。
これ、実は投稿に直接フックしてるわけではないらしいです(詳細は忘れました)。
ともあれ、せっかくのブログですしこういう機能は欲しいわけです。
少なくとも私は欲しかったのでする方法を模索しました。
いろいろ調べる
何はともあれまずは公式です。
そもそもの機能やアドオンでそういったことができるかを調べました。
すると真っ先にこういうのが見つかります。 https://ghost.org/integrations/twitter/
が、読んでるとなんかこれ違くない...?ってなります。
...アレ?見なかったことにしよう...(これ行けるのかな?未検証です)
(以下の私はこれに気づいていません。なんなら読まずにまで飛んでいいです。)
飛ぶならここをどうぞ
traPのGhostはセルフホスティングをしており、そもそもGhostはセルフホスティング方式しかないと思っていたのですが、むしろ普通は公式がホスティングしているアプリケーションに利用者がアカウントを作成し、そこに投稿する方が普通っぽいんですね、↑はそのときに利用できる機能っぽいのです。
私はDocker公式のghostイメージを利用してセルフホスティングしていたので、これは利用できません。
他にも色々調べて見ましたが、私の力では見つけることができませんでした。
なんとかする
じゃあどうしようとなったところで、先程挙げたtraP公式のツイートにヒントを得ます。
ここです。
IFTTTを使えばいいじゃない(IFTTTについては /post/583/ が詳しいです)
IFTTTではなにかをトリガーにツイートをすることができます。
今回の目的でいえば、このトリガーになんとかしてブログ投稿をセットしたいわけです。
ところが、あいにくIFTTTはGhostとの連携機能はありません(あったとしても先程と同様、公式がホスティングしているサービス限定になりそうですが...)。
そのなんとかする手段の一つがWebhookでした。
Ghostでは記事を投稿したことに対してWebhookを送ることができ https://ghost.org/docs/api/v3/webhooks/)、またIFTTTではWebhookをトリガーにすることが可能です。
ここでまた別の問題が発生します。
IFTTTが受け取れるWebhookのフォーマットが固定されているのです。
IFTTTのWebhook問題
IFTTTのWebhookトリガーに対し送ることのできるPOSTリクエストのペイロードは以下のフォーマットに沿ったJSONである必要があります。
{ "value1" : "", "value2" : "", "value3" : "" }
もちろんGhostはそんなこと知ったことじゃないので以下のようなデータが飛びます。
長いです
{
"post": {
"current": {
"id": "5db46b8e62be1c0001a9abb9",
"uuid": "49ca5429-e7a3-4a2a-9a5c-a82c789b43ce",
"title": "test",
"slug": "test",
"mobiledoc": "{\"version\":\"0.3.1\",\"atoms\":[],\"cards\":[],\"markups\":[],\"sections\":[[1,\"p\",[[0,[],0,\"Webhook Test\"]]]]}",
"html": "<p>Webhook Test</p>",
"comment_id": "5db46b8e62be1c0001a9abb9",
"plaintext": "Webhook Test",
"feature_image": null,
"featured": false,
"status": "published",
"meta_title": null,
"meta_description": null,
"created_at": "2019-10-26T15:51:42.000Z",
"updated_at": "2019-10-26T15:51:54.215Z",
"published_at": "2019-10-26T15:51:54.222Z",
"custom_excerpt": null,
"codeinjection_head": null,
"codeinjection_foot": null,
"og_image": null,
"og_title": null,
"og_description": null,
"twitter_image": null,
"twitter_title": null,
"twitter_description": null,
"custom_template": null,
"canonical_url": null,
"authors": [
{
"id": "1",
"name": "caffeine",
"slug": "caffeine",
"email": "xecua@koffein.dev",
"profile_image": "/content/images/2019/09/dotpict_20190911_225551.png",
"cover_image": null,
"bio": "xecua(https://twitter.com/xecual)です",
"website": null,
"location": null,
"facebook": null,
"twitter": "@caffe__ine",
"accessibility": "{\"whatsNew\":{\"lastSeenDate\":\"2019-10-22T10:35:14.000+00:00\"},\"nightShift\":true}",
"status": "active",
"meta_title": null,
"meta_description": null,
"tour": "[\"getting-started\",\"upload-a-theme\"]",
"last_seen": "2019-10-26T14:59:14.000Z",
"created_at": "2019-09-11T12:58:03.000Z",
"updated_at": "2019-10-26T15:03:56.000Z",
"roles": [
{
"id": "5d78ef5b32db3900015b1bf9",
"name": "Owner",
"description": "Blog Owner",
"created_at": "2019-09-11T12:58:03.000Z",
"updated_at": "2019-09-11T12:58:03.000Z"
}
],
"url": "https://blog.koffein.dev/404/"
}
],
"tags": [],
"primary_author": {
"id": "1",
"name": "caffeine",
"slug": "caffeine",
"email": "xecua@koffein.dev",
"profile_image": "/content/images/2019/09/dotpict_20190911_225551.png",
"cover_image": null,
"bio": "xecua(https://twitter.com/xecual)です",
"website": null,
"location": null,
"facebook": null,
"twitter": "@caffe__ine",
"accessibility": "{\"whatsNew\":{\"lastSeenDate\":\"2019-10-22T10:35:14.000+00:00\"},\"nightShift\":true}",
"status": "active",
"meta_title": null,
"meta_description": null,
"tour": "[\"getting-started\",\"upload-a-theme\"]",
"last_seen": "2019-10-26T14:59:14.000Z",
"created_at": "2019-09-11T12:58:03.000Z",
"updated_at": "2019-10-26T15:03:56.000Z",
"roles": [
{
"id": "5d78ef5b32db3900015b1bf9",
"name": "Owner",
"description": "Blog Owner",
"created_at": "2019-09-11T12:58:03.000Z",
"updated_at": "2019-09-11T12:58:03.000Z"
}
],
"url": "https://blog.koffein.dev/404/"
},
"primary_tag": null,
"url": "https://blog.koffein.dev/test/",
"excerpt": "Webhook Test"
},
"previous": {
"mobiledoc": "{\"version\":\"0.3.1\",\"markups\":[],\"atoms\":[],\"cards\":[],\"sections\":[[1,\"p\",[[0,[],0,\"\"]]]]}",
"status": "draft",
"updated_at": "2019-10-26T15:51:42.000Z",
"html": null,
"plaintext": null,
"published_at": null
}
}
}
今回欲しいのはタイトルとURLなので、post.current.id
とpost.current.slug
をうまく取り出せば良いです(URLはoriginにpost.current.slug
を付け加えたものになります)。
Google Apps ScriptとかGoogle Cloud Functions, AWS Lambdaなんかを使ってもいいですが、私は自分自身に送ってサーバー内で処理をしてしまうことにしました。
また、Pythonとかでちゃちゃっと書いても良いのですが、余計なサーバーを増やすのがなんとなく嫌だったので既に利用しているnginxで直接処理することにしました。
nginxでjsonを処理する
といってもnginxの本領はただのWebサーバーです。複雑なデータの処理はできません(たぶん)。
というわけでluaを使えるようにする必要があります。手段としては以下の2つが主になると思います。
lua-nginx-modules
を入れてビルドする- OpenRestyを利用する
自前ビルドはめんどくさかったので後者を選択しました。
luaは一行も書いたことがなかったので、https://symfoware.blog.fc2.com/blog-entry-1972.html を参考に適当に書きます。
local cjson = require "cjson"
-- bodyの解析
ngx.req.read_body()
local req_body = ngx.req.get_body_data()
-- もし取得できていなかったら、データはファイルに行っている
if not req_body then
-- テンポラリのファイル名を取得。内容を全部読み込む
local req_body_file_name = ngx.req.get_body_file()
if req_body_file_name then
local file = io.open(req_body_file_name, 'rb')
req_body = file:read('*a')
file:close()
end
end
if not req_body then
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
-- postされた値を解析
local post = cjson.decode(req_body)
data = { value1 = post["post"]["current"]["title"], value2 = "https://blog.koffein.dev/" .. post["post"]["current"]["slug"] }
local http = require "resty.http"
local httpc = http.new()
local res, err = httpc:request_uri("https://maker.ifttt.com/trigger/{event}/with/key/{key}", {
method = "POST",
body = cjson.encode(data),
headers = {
["Content-Type"] = "application/json",
},
keepalive_timeout = 60,
keepalive_pool = 10,
ssl_verify = false -- todo: fix this
})
if not res then
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
ngx.exit(200)
今思い出したんですけどなんかssl_verify
をfalse
にしないと動かなかったのでそうしてます。たぶん私が悪い。
request_uri
のevent
にはこのイベントの名前を入れます。作成したappletのSettingsで設定できます。
key
には割り振られたkeyを入れます。これは https://ifttt.com/maker_webhooks のDocumentationで確認できます。
最後に、適当にエンドポイントを決め、そこへのリクエストに対する処理としてこのスクリプトを実行するようにnginxを設定します。
content_by_lua_file
とかcontent_by_lua
とかを使えば良いと思います。
あとはGhostのWebhookを設定(Integrations -> Custom Integrationsから新規作成し、Post publishedで作成したエンドポイントに対してPOSTするようにします。触ったことなかったらなんのこっちゃって感じだと思いますし無視していいです)します。
実践
実際にブログを投稿してみると、ちゃんとTwitterにも投稿できたことがわかります。
さいごに
結局内容があるようでない記事になってしまい悲しくなりました。 ブログ作ったのはいいけど今のところ1つしか記事書いてなくてアレな感じになってるのでネタを募集しています。明日はTararaさんとarahi10さんの記事です。お楽しみに。
おまけ
さっき見つけたZapierを試してみました。
ちゃんと出来てますね...悲しい。
まあせっかくなので作ったやつをそのまま使います。
英語だからって投げ出さずに最後まで読みましょうってことですね。