私事ながら、現状、SNS としては X (旧 Twitter)、Bluesky、Mastodon を日常的に使っていて (Threads と Nostr も使っていますが特に Threads は放置気味......)、全部じゃないにしても、一部の投稿については、これらすべてのサービスに同時投稿したいなと、なんとなく思っていました。
巷にはそういう Web サービスも探せばあるとは思うんですが、なるべく自前でやろうということで、週末に思い立って手を付けたら見事に週末の貴重な時間が溶けてなくなりましたけども、なんとか形になったので GitHub で公開しました。
詳しい使い方なんかは README.md
を見ていただければわかると思います。
なんで JSON
投稿のデータを JSON 形式にしたのは、何らかの CMS とかで JSON を更新して、Webhook でスクリプト実行すればマルチポスト捗るんじゃねーかなと思ったからです。色々と汎用性も高そうだし。
簡単ソースコード解説
各 API のドキュメントと、そこにあるサンプルソースコードを参考にしながら書いただけなので偉そうに解説するほどのものでもないのですが一応。
Bluesky
Bluesky 関連で参考にしたのは下記のドキュメントと、GitHub にあるサンプルソースコードです。
あと、URL を単なるリンクとして投稿するだけならよいのですが、リンクカード形式にしようとすると結構面倒。この部分は下記の記事が非常に参考になりました。
具体的には openGraphScraper パッケージを使用して、リンクカードを設定したい URL から og:image
を取得して、そいつを画像としてアップロード後、投稿と紐付けるという作業をしないといけません。
// Open Graph データの取得
const getOgInfo = async (url) => {
try {
const { result } = await ogs({ url: url });
if (!result.success) {
console.log(chalk.yellow('Open Graph データの取得に失敗したので処理をスキップしました'));
return null;
}
const ogImageUrl = result.ogImage?.at(0)?.url || '';
const res = await fetch(ogImageUrl);
const buffer = await res.arrayBuffer();
const mimeType = res.headers.get('Content-Type');
// 画像の MIME Type に基づいて処理を分岐
let imageOptions = { resize: { width: 800, fit: 'inside', withoutEnlargement: true } };
if (mimeType === 'image/jpeg') {
imageOptions = { ...imageOptions, format: 'jpeg', options: { quality: 80, progressive: true } };
} else if (mimeType === 'image/png') {
imageOptions = { ...imageOptions, format: 'png', options: { quality: 80 } };
}
const compressedImage = await sharp(buffer)
.resize(imageOptions.resize)
[imageOptions.format || 'toBuffer'](imageOptions.options || {})
.toBuffer();
return {
siteUrl: url,
ogImageUrl: ogImageUrl,
type: mimeType,
description: result.ogDescription || '',
title: result.ogTitle || '',
imageData: new Uint8Array(compressedImage),
};
} catch (error) {
console.error(chalk.red('Open Graph データの取得中にエラーが発生しました:'), error);
return null;
}
};
参考サイトのソースコードを基に、sharp で画像処理してアップロードする処理などを書きましたが、取得した画像の MIME Type によって処理を変えるみたいな、余計なことをしたのでちょっと処理が複雑になっています。
Mastodon
Mastodon 関連で参考にしたのは下記のドキュメントと、GitHub にあるサンプルソースコードです。
mastodon-api パッケージが最近は全く更新されてないので不安なんですが一旦、これを使わせてもらう事に。
Mastodon 関連で面倒くさかったのは、画像のアップロードで、基本的には下記のような形で画像のアップロードができるんですが、
M.post('media', { file: fs.createReadStream('path/to/image.png') })
Buffer から直でデータを渡せないので、一旦、画像を一時ファイルとしてローカルにコピーしてきて、色々やってからアップロードして、一時ファイルを消す、みたいなクソ面倒くさいことになっています。
多分、この面倒な処理がなければ一番簡単に書けてるかも。
X (旧 Twitter)
X (旧 Twitter) 関連で参考にしたのは GitHub にある、下記のサンプルソースコードです。
X はリンクカードも URL を投稿すれば自動処理だし、アップロードする画像の処理もそんなに難しいことはないのでよいのですが、画像への代替テキストの設定だけ、createMediaMetadata
として独立してたのでちょっとやり方を探すのが面倒くさかったです。
該当部分は下記。画像を取得してきてサイズ変更したり、みたいな処理は別の関数でやってますので、画像をアップロードして代替テキストを関連付ける部分の処理になります。
const uploadImage = async (image) => {
const { processedBuffer, mimeType } = await processImage(image.src);
// 画像をアップロードしてメディアIDを取得
const mediaId = await twitterClient.v1.uploadMedia(processedBuffer, { mimeType: mimeType });
// 画像に代替テキストを設定
if (image.alt) await twitterClient.v1.createMediaMetadata(mediaId, { alt_text: { text: image.alt } });
return mediaId;
};
ということで、私以外の人に役に立つのかはわかりませんが、何かの参考になれば幸いです。
そういえば、Threads も API を近いうちに公開するという話が出ていましたね。API 公開されて Node.js で実装できそうだったら、Threads への投稿用スクリプトも追加してみたいなと思います。そうすれば Threads 放置状態から脱却できるかも。