2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

トゥしたらツイする Mastodon→Twitterクロスポスト(2023年版,画像あり投稿対応)

Last updated at Posted at 2023-05-23

本稿の趣旨

  1. 4ヶ月放置。
  2. にも関わらず「新APIに移行してね!」メールが届く。
  3. ついに旧 API が Suspend された。😊えがお

クロスポストとは

このような環境変化に耐えかねた人々が Twitter から種々の SNS へ避難したわけですが,SNS の本質は人とのつながりです。全員が一斉に移住しない限り,これまで自分をフォローしてくれていた人とはお別れとなります。

ある SNS への投稿があると,それを他 SNS へ自動転載する手法を,ここではクロスポストと呼びます。本稿では,Mastodon へ投稿がなされると,同内容を Twitter へ転載してくれる機能を IFTTT を用いて実装します。

他の類似記事との違いは,画像のあるなしに応じて適切なアクションを選ぶようにしていることと,画像つき投稿を可能とした点です。2017 年頃にこの手の記事が書かれてから現在までに Mastodon 仕様が変わっており,それに追従しました。

準備

  • Mastodon アカウント
  • Mastodon アカウントの </> Development で Application を作成
    • Scopes は read を許可
    • Your access token が必要2023-05-23_22-35-05.png
  • 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

これらを選ぶと IfThen の間に + ボタンが現れるので,それを押して 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 からアクセスできる場所に配置します。pipmastodon.pyurllib3 などを入れておいてください。また,これらのモジュールを 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)を解析すればもっとスマートな実装になるな,ということに気づきましたが,それはまた別の話…

  1. Mastodon → Twitter の連携(IFTTT + GAS) RSS の仕様は変更されたが,画像取得はできないままだ。

  2. 複数のアクションを使うのに Pro プラン契約が必要。

  3. Query と Filter codeを使うのに Pro+ プラン契約が必要。Twitter は無課金ユーザーを貫くが,他サービスには気前よく支払う。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?