feature image

2019年12月10日 | ブログ記事

ブログ投稿ツイートを無理やり自動化した話【AdC2019 41日目】

この記事はAdvent Calendar 2019 41日目の記事です。
本当は別の話をする予定だったんですが内容がなくなりそうだったので急遽変更しました。


一昔前はみんな自分のサーバーにブログ持ってたらしいんですけど最近はあんまりそういう話聞きませんね(はてなとかが充実してるからいらない気もしますが)。というか私はその時代インターネットに触れてなかったのでその話自体知らないんですけど。

ともあれ私の周囲では「みんなブログ持っとる、持ってないのお前だけ」って感じだったので私もVPSを借りてブログを立てました。ブログシステムを自分で作ってる全強とかもいた気がしますが私は全強ではないのでGhost(https://ghost.org/)を利用しました。

投稿ツイートしたい...したくない?

さて、皆さんご覧のこのtraPブログもそのGhostで動いているのですが(これ言っていいのかな)、そこに記事が投稿されると公式Twitterアカウントでその旨がツイートされます。
----------2019-12-10-1.13.42
こんな感じのやつです。
これ、実は投稿に直接フックしてるわけではないらしいです(詳細は忘れました)。

ともあれ、せっかくのブログですしこういう機能は欲しいわけです。
少なくとも私は欲しかったのでする方法を模索しました。

いろいろ調べる

何はともあれまずは公式です。
そもそもの機能やアドオンでそういったことができるかを調べました。
すると真っ先にこういうのが見つかります。 https://ghost.org/integrations/twitter/
が、読んでるとなんかこれ違くない...?ってなります。

----------2019-12-10-1.46.28
...アレ?見なかったことにしよう...(これ行けるのかな?未検証です)
(以下の私はこれに気づいていません。なんなら読まずにまで飛んでいいです。)
飛ぶならここをどうぞ

traPのGhostはセルフホスティングをしており、そもそもGhostはセルフホスティング方式しかないと思っていたのですが、むしろ普通は公式がホスティングしているアプリケーションに利用者がアカウントを作成し、そこに投稿する方が普通っぽいんですね、↑はそのときに利用できる機能っぽいのです。
私はDocker公式のghostイメージを利用してセルフホスティングしていたので、これは利用できません。

他にも色々調べて見ましたが、私の力では見つけることができませんでした。

なんとかする

じゃあどうしようとなったところで、先程挙げたtraP公式のツイートにヒントを得ます。
2019-12-10-1.32.11
ここです。

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.idpost.current.slugをうまく取り出せば良いです(URLはoriginにpost.current.slugを付け加えたものになります)。

Google Apps ScriptとかGoogle Cloud Functions, AWS Lambdaなんかを使ってもいいですが、私は自分自身に送ってサーバー内で処理をしてしまうことにしました。
また、Pythonとかでちゃちゃっと書いても良いのですが、余計なサーバーを増やすのがなんとなく嫌だったので既に利用しているnginxで直接処理することにしました。

nginxでjsonを処理する

といってもnginxの本領はただのWebサーバーです。複雑なデータの処理はできません(たぶん)。
というわけでluaを使えるようにする必要があります。手段としては以下の2つが主になると思います。

自前ビルドはめんどくさかったので後者を選択しました。

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_verifyfalseにしないと動かなかったのでそうしてます。たぶん私が悪い。

request_urieventにはこのイベントの名前を入れます。作成した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にも投稿できたことがわかります。
2019-12-10-18.01.29

さいごに

結局内容があるようでない記事になってしまい悲しくなりました。 ブログ作ったのはいいけど今のところ1つしか記事書いてなくてアレな感じになってるのでネタを募集しています。

明日はTararaさんとarahi10さんの記事です。お楽しみに。

おまけ

さっき見つけたZapierを試してみました。
----------2019-12-10-18.16.34
ちゃんと出来てますね...悲しい。
まあせっかくなので作ったやつをそのまま使います。

英語だからって投げ出さずに最後まで読みましょうってことですね。

xecua icon
この記事を書いた人
xecua

音ゲーマーじゃないです

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2019年12月21日
モデリングを始めてみたい君へ、MagicaVoxelのススメ
isak icon isak
2019年12月13日
ゲーム紹介「League of Legends」【AdC2019 44日目】
Yataka_ML icon Yataka_ML
2019年12月25日
TensorFlow.jsでwasmを使ってみるためにコントリビュートした【AdC2019 56日目】
sappi_red icon sappi_red
2019年12月25日
無料でDTM環境構築 電子音系編
liquid1224 icon liquid1224
2019年12月4日
部内製チャットサービス「traQ」UIのこれまで 【AdC2019 35日目】
spa icon spa
2019年12月23日
無料でDTM環境構築 生音系編
kashiwade icon kashiwade
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記