Featured image of post Twitterの投稿リクエストを模倣してMisskeyの投稿を転送するツールをDenoで作った

Twitterの投稿リクエストを模倣してMisskeyの投稿を転送するツールをDenoで作った

目次

はじめに

この記事は、Misskey Advent Calendar 2023 2枠目 12月13日用に書いたものです。

自分用のインスタンスを立てるくらいにはMisskeyを使っているので、参加できて嬉しいです。

MisskeyのWebhook機能を利用して、MisskeyでのノートをTwitterに投稿を転送する(クロスポストする)ツールを作ったので、Misskeyアドベントカレンダーという機会を利用して公開します。

注意本記事ではX (SNS)のことを「Twitter」、ポストのことを「ツイート」などと表現していますので、必要な方は適宜読み替えてください。

作った背景

友達の北沢さんが、MisskeyでのおもしろいノートをTwitterでも投稿したいという理由で、IFTTTを用いてMisskeyのノートをTwitterに転送していました。

しかし、Twitter APIの料金体系の変更と、それに伴うIFTTTのTwitterアプレットの有料化によって、北沢さんの投稿が転送されなくなってしまいました。

Updates to IFTTT free tier - IFTTT

かわいそう

私は北沢さんのおもしろい投稿が大好きなので、転送ツールを作ってあげることにしました。

作ったツール

msk2twという名前です。直球すぎるわかりやすく無駄のないネーミング。

TypeScriptで書いてDenoで動かします。また、HonoというWebフレームワークを使用しました。

ソースコードは以下で公開しています。

MateChan/msk2tw

仕組み

本当に簡単な仕組みです。

MisskeyのWebhookを、msk2twが動いているDeno DeployのURLに飛ばします。その情報と、あらかじめTwitterのCookieから引っこ抜いておいたトークンをもとに、msk2twがブラウザ版Twitterのツイートリクエストを模倣し、Twitterにも投稿します。

こういう図を描いてみたかっただけ

使い方

リポジトリをフォークするなどしたあと、Deno Deployの新規作成 → Deploy your own codeから、msk2twをDenoプロジェクトとしてデプロイします。

その他はGitHubのREADME.mdにあるとおりです。

msk2tw/README.md at main · MateChan/msk2tw

Misskey Webhook

今回作ったツールは、MisskeyのWebhook機能を大いに活用します。これがなかったら作っていないかも。

MisskeyにはWebhookが用意されています。Webhookを利用すると、Misskey上の様々なイベントをリアルタイムに受け取ることが可能です。

Webhook | Misskey Hub

Misskey Webhook

これにより、ノートを投稿した、リアクションされた、みたいなイベントが発生したときに、その情報を指定したURLに飛ばすことができます。

ノートの投稿をトリガーとしてWebhookを飛ばすと、ノートのテキスト、公開範囲、添付されたファイルなどの情報を、以下のような構造で取得することができます。以下はだいぶ端折っていて、本当はもっとたくさんのデータを取得できます。

{
  type: "note",
  body: {
    note: {
      user: { ... },
      text: "ぽぽぽ",
      cw: null,
      visibility: "public",
      localOnly: false,
      files: []
    }
  }
}

このようなデータから必要な値を取り出し、その内容をもとにTwitterにも投稿する、という順序ですね。ノートの本文であるtextはもちろん、パブリックな投稿のみを転送するためにvisibilitylocalOnlyなどの値も必要です。

Web版Twitterのツイートリクエストを解析する

自動でTwitterに投稿するためには、Twitterの開発者登録みたいな手続きをしなくてはいけません。

いけませんが、あー、めんどい。いや、めんどくさくはないのですが、「どうして私がTwitterなんかのために正当な手順を踏んで開発者登録をしなければいけないんだ」みたいな気持ちになります。

そこで、「ブラウザ版Twitterのツイートリクエストを解析して、それを模倣すればいいのでは?」と考えました。

トークン

ということで、Web版Twitterで実際にツイートして、ブラウザの開発者ツールからfetchの内容を覗きます。覗いたfetchリクエストから必要な内容を取り出したり、アカウントによって異なる部分を探したりします。

その結果、auth_tokenct0というパラメータが、アカウントによって異なることがわかりました。これらの値はブラウザのCookieに保存されているトークンです。これらのトークンを用いることで、ツイートの投稿を含めたあらゆる操作が可能になります。

クッキー☆

リクエストURL

開発者ツールで見ると、ツイートリクエストは以下のようなURLに飛んでいることがわかりました。

https://twitter.com/i/api/graphql/I_J3_LvnnihD0Gjbq5pD2g/CreateTweet

しかし、このURLのI_J3_Lv...の部分は、どうやら一定期間で変わるようです。私が実際に試しても、確かにURLが変わっていることがわかりました。

すてさん (@s@honi.stesan.dev)

どうしたものかなあと思っていたとき、OldTweetDeckのソースコードを覗いたところ、tTsjMKyhajZvK4q76mpIBgという固定の値を使用していることがわかりました。

ほんとに?と思って、実際にこのURLに投稿リクエストを飛ばしてみると、正しくツイートできました。そんなことあるんだ。

ツイート関数を作る

以上を踏まえた上で、ツイートしたいテキストとこれらのトークンを引数とし、ブラウザのツイートリクエストを模倣するtweet関数を作れば良さそうです。

で、こんな感じです。

export const tweet = async (text: string, authToken: string, ct0: string) => {
  const url =
    "https://twitter.com/i/api/graphql/tTsjMKyhajZvK4q76mpIBg/CreateTweet";
  const authorization =
    "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA";
  const body = {
    variables: {
      tweet_text: text,
    },
    features: {
      tweetypie_unmention_optimization_enabled: true,
      responsive_web_edit_tweet_api_enabled: true,
      graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
      view_counts_everywhere_api_enabled: true,
      longform_notetweets_consumption_enabled: true,
      responsive_web_twitter_article_tweet_consumption_enabled: false,
      tweet_awards_web_tipping_enabled: false,
      longform_notetweets_rich_text_read_enabled: true,
      longform_notetweets_inline_media_enabled: true,
      responsive_web_graphql_exclude_directive_enabled: true,
      verified_phone_label_enabled: false,
      freedom_of_speech_not_reach_fetch_enabled: true,
      standardized_nudges_misinfo: true,
      tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:
        true,
      responsive_web_media_download_video_enabled: false,
      responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
      responsive_web_graphql_timeline_navigation_enabled: true,
      responsive_web_enhance_cards_enabled: false,
    },
  };
  return await fetch(url, {
    headers: {
      "content-type": "application/json",
      "authorization": authorization,
      "x-csrf-token": ct0,
      "cookie": `auth_token=${authToken}; ct0=${ct0};`,
    },
    body: JSON.stringify(body),
    method: "POST",
  });
};

意外とシンプル。

工夫(妥協)した点

本ツールを作るにあたり、以下のような点を工夫(妥協)しました。

公開投稿のみを転送する

公開範囲を限定したノートがTwitterに放流されてしまうのはかなりよくないので、公開範囲が限定されていない場合のみに転送するようにしました。

ファイルや画像の処理

Twitterで画像を添付したツイートをするには、ツイートリクエストにメディアIDを付与すればいいのですが、メディアIDをゲットするにはTwitterのサーバーに画像をアップロードする必要があります。

そのアップロードする処理をどうすればいいかよくわからなかったので、ファイルや画像をURLで本文に加えることにしました。

絵文字 + 画像URL で表現

作った感想・今後の展望

作ったツールを実際に北沢さんが使ってくれて、喜んでくれました。やったー!

「ぽみゃーら」は「お前ら」の意味だと思います

今後の展望としては、特定の単語を含む投稿のみを転送する機能を搭載したり、転送のオンオフを簡単に切り替えられるようにしたりしたいと思っています。思っていますが、そもそも利用者が2人くらいしかいないので、どうしようかなあ……。

DenoとHono

上でも述べましたが、本ツールはDenoとHonoにより動作しています。

DenoはJavaScriptランタイムです。もともとGoogle Apps Scriptをよく使っていたこともあり、書いたTypeScriptコードがそのまま動くDenoはとっつきやすい感じがします。あとファイル数も格段に少なくて済むので、心もディレクトリも清々しいです。

そして、Deno Deployという、書いたコードを簡単にホスティングするサービスも提供されています。書いたコードをすぐに動かせるPlaygroundもあるので、本当に気軽に試せます。

Deno, The next-generation JavaScript runtime

また、HonoはUltrafast & LightweightなWebフレームワークです。こちらも非常に使いやすいフレームワークで重宝しています。

Hono - Ultrafast web framework for the Edges

おわりに&宣伝

Misskeyアドカレの記事なのに、半分くらいTwitterをハックする内容になってしまいました。すみません。

Misskeyは、Webhookなどの機能をはじめとして、ユーザー側が自由にツールを作れる環境が整っています。また、APIの仕様が公開されていたり、日本語の公式ドキュメントが整備されていたりなど、このようなツールの開発に対して非常に敷居が低いように感じます。本当にありがたいことです。

私のように、プログラミング経験が浅いけどなんか作ってみたいという人には、Denoを使ってMisskey用のツールを作ってみるというのは、とてもおすすめです!


最後になりますが自己紹介と宣伝です。

まてちゃん(まてかす、MateChan)という名前で、TwitterやMisskeyをフラフラしています。

Misskeyは自鯖を立てるくらいには楽しませてもらっています。関係者の皆様ありがとうございます。

【超簡単】余った中古デスクトップをMisskeyサーバーとして再利用

Android端末やゲーム機をいじることを趣味としております。このブログでも、Android端末に関係あることやないことをシェアしています。ぜひ他の記事もチェックしてみてくださいね。

まてかすのメモ帳 トップページ


2024年も、Misskeyとともに楽しい一年を過ごせますように……。

以上です、おわり。

Hugo で構築されています。
テーマ StackJimmy によって設計されています。