本稿の趣旨
- 4ヶ月放置。
- にも関わらず「新APIに移行してね!」メールが届く。
- ついに旧 API が Suspend された。😊えがお
クロスポストとは
このような環境変化に耐えかねた人々が Twitter から種々の SNS へ避難したわけですが,SNS の本質は人とのつながりです。全員が一斉に移住しない限り,これまで自分をフォローしてくれていた人とはお別れとなります。
ある SNS への投稿があると,それを他 SNS へ自動転載する手法を,ここではクロスポストと呼びます。本稿では,Mastodon へ投稿がなされると,同内容を Twitter へ転載してくれる機能を IFTTT を用いて実装します。
他の類似記事との違いは,画像のあるなしに応じて適切なアクションを選ぶようにしていることと,画像つき投稿を可能とした点です。2017 年頃にこの手の記事が書かれてから現在までに Mastodon 仕様が変わっており,それに追従しました。
準備
- Mastodon アカウント
- Mastodon アカウントの </> Development で Application を作成
- IFTTT Pro+ アカウント いきなり壁が分厚く高くなりましたね……
- Mastodon.py モジュールを使用できる Web サーバ さらに壁を強化するスタイル
- Twitter アカウント
実装
全体の流れ
最初に処理の流れを示します。
Mastodon は投稿があると RSS 形式で更新情報を配信します。この RSS Feed を IFTTT の New feed item
トリガーに指定し,以後の処理を行います。IFTTT には Twitter 投稿アクションが整備されており,「テキストのみ版(Post a tweet
)」と「画像つき版(Post a tweet with image
)」があります。本来であれば,RSS Feed から得られた EntryImageUrl
の値を参照して画像のありなしに応じ Filter code で処理を分ければすむ……としたかったところですが,IFTTT の RSS Feed トリガーでは Mastodon の画像 URL は取得できない仕様のため1,画像 URL を Webhook を利用して取得します。
なぜ Webhook なんだ,もっとイケてるサービスがあるだろう,というご指摘はおありかと思いますが,Google Apps Script を使いこなすのには時間がかかりそうだなと思ったのと,Jupyter Lab やら GitLab やらをすでに稼働しており,Webhook 用の HTTP サーバを準備するのにたいした手間がかからなかったので……。あと Google はよく突然のディスコンを発表するので,GAS がそうなると厄介だなと思ったのも理由です。突ディスは Twitter API だけでもうお腹いっぱいです😊
IFTTT の設定
アプレット
Applet を作成し,以下の図のように配置します。
Trigger (If This) : RSS Feed - New feed item
Action (Then That) : Twitter - Post a tweet と Twitter - Post a tweet with image2
これらを選ぶと If
と Then
の間に +
ボタンが現れるので,それを押して Query と Filter を配置3。
With (Query) : Webhooks - Make a web request with JSON response
When (Filter code) : 後述のスクリプトを書く。
Trigger : RSS Feed - New feed item
Feed URL には Mastodon の RSS Feed URL を指定します。https://<インスタンスのホスト>/@<ユーザ名>.rss
で取得できると思います。例えば,私のアカウントがある mstdn.jp であれば https://mstdn.jp/@rino.rss
といった感じです。
Action : Twitter - Post a tweet / with image
どっちを先にしても構いません。Twitter account には IFTTT と連携している投稿先 Twitter アカウントを設定します。Tweet text は Add ingredient から EntryContent
を選んでください。Image URL は Filter code で上書きするので何でも構いません。
With : Webhooks - Make a web request with JSON response
URL には Mastodon の投稿からメディア URL を引っ張ってくるスクリプトを置いた URL を設定します。Method は POST,Content Type は application/x-www-form-urlencoded,Body は以下のように設定します。
{
"EntryUrl": "<<<EntryUrl>>>"
}
なお,私はこの Webhook で本稿に書いていない処理も行っているので,実際の運用では少し違う設定をしています。
When : Filter code
以下をコピペしてください。本文中に nocrosspost
(case-insensitive)が書いてあると,その投稿はクロスポストしないようにしてあります。
// Tweet when toot.
// This filter code skips tweeting if 'nocrosspost' is present
// in the given content.
// Use postNewTweet() if the content does not include an image.
// Use postNewTweetWithImage() if does include.
// Note that only the first image in the content is cross-posted.
let content = Feed.newFeedItem.EntryContent;
let payload = JSON.parse(MakerWebhooks.makeWebRequestQueryJson[0].ResponseBody);
let imageUrl = `${payload.EntryImageUrl}`;
let noImage = 'no_image_card.png';
// if nocrosspost is found in the content, skip actions
if (content.toLowerCase().indexOf('nocrosspost') !== -1) {
Twitter.postNewTweet.skip('no cross post');
Twitter.postNewTweetWithImage.skip('no cross post');
}
// then post
else {
// no image -> use postNewTweet()
if (imageUrl.indexOf(noImage) !== -1) {
Twitter.postNewTweetWithImage.skip('no image found');
}
// with image -> use postNewTweetWithImage()
else {
Twitter.postNewTweet.skip('an image is given');
Twitter.postNewTweetWithImage.setPhotoUrl(imageUrl)
}
}
Webhook で呼び出す CGI スクリプト
CGI スクリプト本体
以下を Webhooks - Make a web request with JSON response で指定した URL からアクセスできる場所に配置します。pip
で mastodon.py
や urllib3
などを入れておいてください。また,これらのモジュールを HTTPd のユーザー(www-data
とか)が使えるよう,PYTHONPATH
を設定するか,system-wide にインストールするなどしてください。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Webhook for IFTTT
# Receive EntryUrl, send EntryImageUrl
import cgi
import dotenv
import io
import json
import mastodon
import os
import sys
import urllib.parse
# read dotenv
dotenv.load_dotenv()
# use utf-8 for stdout
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
def retrieve(url):
toot_id = url.split('/')[4]
media_files = []
try:
# create mastodon object
mstdn = mastodon.Mastodon(
api_base_url=os.getenv('EIU_API_BASE_URL'),
access_token=os.getenv('EIU_ACCESS_TOKEN'),
)
# obtain the content of toot_id
status = mstdn.status(toot_id)
# add url to media_files if images are attached
for media in status.media_attachments:
if media.type == 'image':
media_files.append(media.url)
except Exception as e:
status = e.args
return status, media_files
# obtain the url of newly created post
if os.environ['REQUEST_METHOD'] == 'POST':
# parse EntryUrl
length, _ = cgi.parse_header(os.environ['CONTENT_LENGTH'])
query_string = sys.stdin.buffer.read(int(length))
data = json.loads(query_string.decode('utf-8'))
url = urllib.parse.unquote_plus(data['EntryUrl'])
# obtain EntryImageUrl
status, media_files = retrieve(url)
# return data to IFTTT
if not type(status) is tuple:
for i in range(4 - len(media_files)):
media_files.append('no_image_card.png')
print('Content-Type: text/json; charset=utf-8')
print('Access-Control-Allow-Origin: *')
print("\r\n\r\n")
print('{"EntryImageUrl" : "%s", "EntryImageUrl2" : "%s", "EntryImageUrl3" : "%s", "EntryImageUrl4" : "%s"}' %
(media_files[0], media_files[1], media_files[2], media_files[3]))
else:
print('Content-Type: text/json; charset=utf-8')
print('Access-Control-Allow-Origin: *')
print("\r\n\r\n")
print('{"value1" : "%s"}' % ':'.join(map(str, status)))
.env
また,この CGI スクリプトと同じ,または親ディレクトリのどこかに .env
ファイルを以下の内容で作成してください。
EIU_API_BASE_URL="https://mstdn.jp" # 自分のインスタンスに合わせて変更
EIU_ACCESS_TOKEN=" " # Mastoron API 用のアクセストークン
まとめ
Mastodon の投稿を,画像あるなしを判定し,Twitter へクロスポストする仕組みを IFTTT Pro+ を使って実装しました。本稿を書いていて,Mastodon の RSS Feed(XML)を解析すればもっとスマートな実装になるな,ということに気づきましたが,それはまた別の話…
-
Mastodon → Twitter の連携(IFTTT + GAS) RSS の仕様は変更されたが,画像取得はできないままだ。 ↩
-
複数のアクションを使うのに Pro プラン契約が必要。 ↩
-
Query と Filter codeを使うのに Pro+ プラン契約が必要。Twitter は無課金ユーザーを貫くが,他サービスには気前よく支払う。 ↩