Bluesky&Twitterの同時投稿サービスを作り、公開した。Zennねこの🦋Bluesky&Twitterの同時投稿サービスを作り、公開した。追記:WebShareAPIというのを採用したおかげで、ついにXへの(同時ではないが)ほぼ完全なクロスポストが可能になりました!実際の操作例については、こちらの動画をご覧ください。https://www.youtube.com/shorts/6rcgH_-EHRA一方、WebShareAPIが使えない環境もありますので、そちらでは引き続きOGPジェネレータとしてご利用ください。追記:本当にありがたいことにBluesky内で非常に注目を集めることになり、現在は開発に対して非常に積極的です!まだリリースして一週間もしていませんが、すでにAPI実行数(=OGP生成回数)は4000回を超えており、非常に多くのアクティブユーザ(Blueskyのテスターやアーリーアダプター)に使用していただいております。元Postはこちら:本文が悲観的な状態の際に読んでくれた方にはご心配をおかけしました。以降本文全体的に修正を入れてます。内容自体はそんなに変わりません。...BlueSkyをご存じでしょうか。Twitter(現X)の共同創業者の人が作った次なるSNSで、現在絶賛βテストをやっています。招待制が終了、引き続きテスト中ではあると思いますが、ひとまずリリースされました。最近アイコンがちょうちょになってとってもかわいい。βテスト段階で運よくWaitlistから招待コードが発行され、Blueskyデビューしました。開発者の方や、フィードバックをくれそうな一般使用者は率先してフォローお返ししてます。お気軽にフォローしてね。そんなBlueskyですがUIがTwitterにとても似ており、これは流行ると...勝手に思い込み、せっかくだから自分に何かできないかと考え、Bluesky&Twitterのマルチポストサービスを作りました。広まると思いきや絶賛閑古鳥が鳴いてます[1]。リリースしたばかりというのもありますが、それでもまだ趣味のブログの方が注目されてます。流行らなそうだな...と思い心が折れかけてしまってるので、いったん開発の手を止めて文章を書いている次第です。ありがたいことに皆さんに広めていただき、大変うれしく思います。本文章ではSkyshareを作った経緯、そしてこれを支える技術について、後続の開発者の皆様や、これから開発を始めたい新米デベロッパーに向けて記載していきます。経緯Blueskyのアイコンがちょうちょに変わったころ、私も重い腰を上げてアプリをダウンロード&登録しました。WaitListに登録して3週間ぐらいで招待コードが届いたので「意外と早いな」と思いました。これが事の始まりで、こんなに簡単に登録できるのにユーザが移動しないのはなんでだろう、去年ぐらいに話題になったからみんな知ってるはずなのに...という思考から最終的に、アーティストはユーザが居るSNSに、ユーザはアーティストが居るSNSにという構造が出来上がってしまっているから人が動かないのではないか?という結論に至りました。実際は「本当にたまたま私は招待コードを早く受け取れただけ」であり、ユーザが移動できないのはコンテンツ以外の要員がまあまああったのですが、当初の私は、招待制であること自体はユーザの行動意欲を減らす要因にはならないだろう、と思い込んでました。これは浅はか...前例があったのに何も学んでいない[2]簡潔にまとめると、Blueskyに参加できて嬉しくなってハイになったから作りました。動機アーティストはユーザが居るSNSに、ユーザはアーティストが居るSNSにという構造が出来上がってしまっているから人が動かないのではないか?という結論に至りました。これを解消するためには、やっぱりまずは両方の土俵をそろえる必要があると思いました。今やSNSなんて使い分けるほどありますが、Twitterほど汎用的でなんでも投稿できる(諸説ありますが)SNSはないと思います。Blueskyはそれになりえると考えました。クリエイターが両方にコンテンツを提供すれば土俵は同じになるはずです。広告やおすすめでめちゃくちゃになったXのタイムラインではなく、Blueskyで快適に昔のTwitterにみんな戻りたいと思っているはずだ[3]...という思考で、BlueskyとTwitterに同時投稿できたらいいのでは?と考えに至ります。開発開始の頃(2024年1月頭ぐらい)見る限りはChromeの拡張機能として上記を実現しているツールが存在していました。Chromeの拡張機能であるため、スマホやタブレットでの使用はできません。この課題をなんとかしているサービスはありません。今回開発に至ったSkyshare.ukは、ユーザに自由を与え、より快適なプラットフォームへ遷移させる選択肢を与えることを目的にしています。その対象がX→Blueskyだったというだけです。Xでクリエイティブな活動をされている人が、Blueskyへのコンテンツ供給を本ツールを用いて行うことで、BlueskyとXの両方にコンテンツを供給します。いうと、クリエイターはオーディエンス(ROMなどの一般ユーザ・本説明では単にユーザと書きます)の数などの関係で、Xを切り捨てることが困難であるためです。クリエイターのコンテンツを受領するプラットフォームをユーザに選択する余地が生まれます。コンテンツ主体のプラットフォームにおいて土俵を一致させることで、XとBlueskyを競合させます。ユーザはより快適なプラットフォームを選びます。これがXかBlueskyかはユーザに委ねられます。ユーザの増加により、既存クリエイターの定着や、ユーザ増加により魅力が上がったプラットフォームへさらに新規クリエイター流入することが期待できます。この1〜4の工程の好循環により、クリエイターとユーザの幸福を実現します!(ついでに片方のプラットフォームが栄えます。)TwitterAPIの課題Twitterは去年頃から実質デベロッパーの締め出しといえるAPI料金を提示してきました。どんなに安くても一か月に$100(日本円で15000円ぐらい)は支払う必要があります。この結果、ほとんどのサードパーティTwitterクライアントが開発を終了しちゃいました。Twitterへの投稿をどうするかTwitterAPIを使っての開発は(月15000円回収できるサービスか、慈善活動でもない限り)ほぼ無理という状況です。ゆえに、Twitterへの自動投稿はあきらめざるを得ませんでした。同時投稿自体が不可能ではないと考え、できるだけユーザの手を煩わせない形で、いわゆる半自動でBlueskyとTwitterへの同時投稿を実現すれば、まだ希望はあるのではないでしょうか。Button)」を使って、半自動でのBluesky/Twitterの同時投稿を実現することを考えました。Buttonについてこういうやつです。Button)の例これなら本文の再入力は抑えられますが、添付のメディアが主体であるクリエイターにとってはあまり有用ではないと感じました。そこで、メディアについてはOGPを採用してごまかすことで対処しようと考えました。OGPとは聞きなれない言葉だと思います。私もWeb開発を始める前までは知りませんでした。要はURLにくっついてくるサムネイルのことです。Protocolなので、正確には画像にくっついてくるサムネイルは「OGPカード」などと呼ばれるらしいですが知ったこっちゃないです。Blueskyの画像の投稿からこれを作ることができれば、完全な同時投稿とは言えなくてもかなり要件は満たせているのでは?と考えました。これが今回作成したWebサービス「Skyshare」の動機であり、機能であり、すべてであります。...実際はこんな簡単な話じゃなかったので全然流行ってないんですけどね。課題はありました[5]が、今回のサービスはリンク先は関係ない、リンク先ではなくリンクから取得可能なOGPカードの部分にのみ用事があったため、いったん目をつむりました。開発についてWebサービスではありますが、個人として初めての開発であり信頼が薄いこと、またBlueskyのAPIは始まったばかりで、新参開発者の方は苦労されているだろうと思い、ソースコードを全て公開しています。同じものがつくれますし、公開している以上、不正だと考えられるコードは一切記載しない努力をしています。Protoのかかわるフロントエンドだけ公開にしようかな、とも思ったのですが、勉強される方も全部見えた方がうれしいですよね。いうことで、バックエンドもリポジトリに追加しています。Protocolだけにとどまらず、公式クライアントですらOSSです。これってとんでもないことだと思います。いう部分もBlueskyが期待できる点です。使ったサービス開発していて色んな選択肢を見ていると、知らないだけで優良なサービスはごろごろしてますね。今回は以下のIaaSやPaaSを使用しました。Upstashには助けられました。Worker無料枠(たった10msしか実行可能時間がない)でも問題なく動作する、無料枠が良心的だったりと、とんでもないサービスです。サービスのマッピング以下のような感じになります。注記すべきは以下です。UpstashDBからの情報取得はCloudflare越しに実施しています。アクセストークンの保持の観点でセキュリティ的に問題があるためです。FirebaseでもBlueskyから情報を得ています。クライアントに終始しないのは、クライアントサイドのスクリプト改ざん等、セキュリティ的な問題への対処のためです。詳しいこと書きたいことは山ほどあるのですが、全部説明するには余暇がたりません。よければソースコードを読んでいただければと思います。解説を書く時間はないのですが、逆引き的にいくつか難しい部分のソースコードを引っ張ってきておきます。現在Skyshareはバージョンがv1.4.11とかなり上がっており、本ドキュメントの内容が陳腐化している可能性があります。参考レベルにとどめていただければ幸いです。createPortalによるコンポーネントの別箇所配置こちらでcreatePortalを実施しています。https://github.com/nkte8/skyshare/blob/main/astro/src/components/Client/index.tsxどうしてこのような形をとったかというと、(root)/app/ページ以外でもセッションを利用したかったからです。具体的には(URL)/posts/hogehoge/ページで、ページの削除を実施する際にセッションを取得するのですが、このためだけにクライアントをポータル化して、どのページでも展開できるようにしています。SSRモードで、データベースからページを生成するAstroのSSRモードはサーバーサイドレンダリング、つまり動的にページを生成できます。これはどういう話かというと、例えばDBに登録された情報を使ってページが生成できるという感じです。以下では(root)/posts/(アクセスされたページ名)/について、DBへ問い合わせを行いレスポンスが正しければ通過する、というコードです。考え方としてはネガティブフィルタリングで、アクセスされたくないページに対して503や404レスポンスを行います。https://github.com/nkte8/skyshare/blob/main/astro/src/pages/posts/[slug].astroBluesky向けの投稿のURLやメンションをきちんと実装する@atproto/apiライブラリさえ使えばdetectFacets関数で簡単にRecordを作成できますが、今回はすべてRESTを直接たたく実装をした(理由は後述)ので、detectFacets相当の機能は自力で実装しました。処理としては正規表現でURLやメンションの内容を確定して、app.bsky.richtext.facetの定義に従ってObject型を作成・Recordに付与しているだけです。RegExp型でgオプションを付けてしまうとbyteStartに苦労するので、正規表現は一要素ずつ再帰関数により情報の取得・作成をしています。byteStartとbyteEndはUTF-8のバイト数である必要があります。日本語の場合は3byteなので、byteStart~byteEndでは3カウント分になります。この計算は、本文をencodeURI()関数を用いてURIエンコードを行い、%..部分を一文字に置き換える(日本語は%..%..%..とエンコードされるため、これにより3カウント分取得できる)ことで文字数を数えています。})}app.bsky.richtext.facetの構造は以下のようになっています。linkもmentionも似たような型をしているので、もっと効率化できそうです。https://github.com/nkte8/skyshare/blob/main/astro/src/utils/atproto_api/facets.tsただ、単純に複合型というわけには行きません。facetの作成には、RegExpで取得した結果を以下のようにマッピング処理をしていますが、もしapp.bsky.richtext.facetの型を一括に扱うのであれば、これらの関数も統合するべきだからです。result}ここの美しい実装方法はまだ思いつきません。アイデアはいつでも募集中です!躓いた点Reactで@atproto/apiパッケージが動かない@atproto/apiはWebアセンブリに対応していないため[6]、残念ながらReactのWebコンポーネントにバンドルすることはできません。クライアントが使えないならクライアントを使わなければいいじゃない、ということで、SkyshareではRESTをfetchAPIで直接たたくことで解決しました。ソースコードでは以下の処理をRESTで使うことができます。メディアのアップロード、アップロード後の識別子(ref.$link)を取得Firebaseで@upstash/redisが動かないうまく動かせませんでした。原因は最後まで分からず...幸い、Upstash上のRedisはRESTにより操作が可能だったため、こちらもfetchAPIを直接たたいています。REST、RESTがすべてを解決する。ちなみにRESTのたたき方自体はUpstash準拠なのですが、たたくコマンドについてはRedis側の命令に準じている仕様で面白いです。REST_URL/get/foo...ソースコードでは基本的な値の登録と削除をRESTで実施しました。Cloudflare側はクライアントがきちんと動作したので、@upstash/redisを使っています。苦労したかもしれません。Workerで設定方法が異なります。})として設定される必要があります。以下の例ではRegex型として定義しています。Workerについては何のひねりもなく、レスポンスのヘッダにAccess-Control-Allow-Originを指定してあげるだけですが、注意すべきはURLは/で終わってはいけません。Workerのように正規表現で楽もできそうにないです。(詳しい方いらっしゃったらコメントお願いします。)以下の例だとskyshare.ukのみCORS設定をしたい場合は以下のように設定します。corsHeaders});}実際に使ってみてちゃんとBlueskyに画像は投稿されますし、Twitter用のOGPカードも生成されて、両方のSNSへの投稿は非常に簡単になりました。...投稿自体は簡単になったのですが、体感Twitterでのインプレッションやいいねが全然されず、Blueskyの方はBlueskyの方でフォロワーが全然いないのであまり楽しくなく、なんだか中途半端になってしまいました。使ってるユーザ(私)自身の問題な部分はあるかもしれません。この課題はツールがたくさん使われるようになっても、やはり多くのクリエイターが気にしているようでした。今後の開発についていったん作りたいものは作ったので個人的には満足です。Tokenの実装など)が必要なのですが、ユーザが増えるまでは急ぐことではないと判断して正式リリースには含めませんでした。あとはボタン表示や投稿画面をもうちょっとリッチにするといった、見た目のほそぼそとしたアップデートが主になる見込みです。利用していただいており、今のところは順調に使われています。現在の時点で、APIの実行には認証の意味も兼ねてBlueskyのアクセストークンやリソースが必要な作りをしていますが、万が一に備えてAPIをより強固に守る仕組みや、ユーザフィードバックを反映してUIの改善や機能の追加を頑張っていきたいです。1Q~2Qと記載が変わっていたので、近いうちにメディア投稿もできるようになるのだと思います。これがTwitterの狭い無料範囲枠で使えること、狭い無料範囲枠が少しでも広がってくれたらいいな...と思いますが、期待はしないでおきましょう。Blueskyに行こうね...文章の最初の方でも載せましたがあらためて。Skyshareのアプデ情報や、技術的に面白そうだな~みたいな興味や、こうしたいああしたいというような情報を発信しています。あと、距離感バグったみたいなリプライをフォロー/フォロー外限らず結構してます。個人制作のプロダクトだから、公式みたいなお堅さは要らないだろうという部分で、ユーザと近いコミュニケーションのがいいと思っているからです。実際その方が楽しいからね。https://bsky.app/profile/nekono.devあとは今回学んだことを趣味のWebサイトの拡充などに活かしていきたいです。今回はBlueskyのRESTについては、書き込み系ばかりで読み取りをやっていなかったので、次はbotの製作に挑戦してみたいです。2024-02-05この記事、深夜しょんんぼりしながら書いたので卑下して、初版は「BlueskyとTwitterを繋ぐサービスが作りたかった」とタイトルを設定していましたが、今朝起きてよくよく考えたら「作った」ことに関しては事実なのでタイトルを「Bluesky&Twitterへ同時投稿するサービスを作った」に修正しました。2024-02-08タイトルをBluesky&Twitterの同時投稿サービスを作り、公開した。そのままでも良かったんですけど、明確に更新したことが分かった方が、読む側もとっつきやすいかなと思ったためです。ここまで読んでくれてありがとうね。2024-04-13目的と手段セクションを作って、本アプリがどういうことを期待しているのかを明文化しました!また、本当に遅くなりましたが、本記事へバッジを貼っていただいた皆様、本当にありがとうございます!非常に励みになります!現在、開発はより規模を大きくしており、ZEKE320氏やSo-Asano氏の協力により、OSSとしての品質向上や、機能の実現の幅を広げています。Blueskyが盛り上がっていってほしい、そして、クリエイターやユーザのみなさんの幸福のお手伝いができたらいいなと、思っています!脚注2024/02/04時点のGoogleアナリティクスおよびTwitterでのドメイン名検索結果から。ドメインはBskylinx→Skyshareとサービス名が一度変わっているため、これも含めて検索したところほとんど自身の利用履歴で悲しかった。skyshare.ukでTwitterを検索してはニヨニヨしてます。Clubhoseは肉声を使うことや通話という体系上時間の制約があるなど、そもそもの特性上難しい点があり、招待制でなくても流行っていなかったと考えていました。最近は自分のおすすめが猫と文鳥でいっぱいになって、これはこれでいいな...と思うとともに、Blueskyのフィードの不完全さを嘆いています。↩︎SocialHubがギリギリ対応していたかと思ったのですが、去年の夏ごろにやはりTwitter連携は終了していました。↩︎URL付きの投稿のインプレッション(投稿自体の表示数)は、URL無しの投稿のインプレッションよりも少なく出るという検証結果があります。【検証】TwitterでURLを記載するとインプレッションが低下するのは事実?。クリック率についてはあまり高くないという理解はありましたが、まさかインプレッションにも影響があるとは思ってませんでした。↩︎ReactやAstroでbuildができないライブラリ全般、Webアセンブリに対応していないことが原因だと理解していますが、もし違ったらコメントで指摘してください。Web開発歴4ヶ月ちょっとしかないため、間違ったことを書いている可能性は十分にあります。↩︎ねこのいろいろやってます。nkhnd.hatenablog.jpバッジを贈って著者を応援しようバッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。2024/02/07に更新すみませんなかなか更新できてなくて文章悲壮感溢れてますが、今めちゃくちゃ注目されてます。(絶賛バズ中)ありがとうございます!!アップデート等もしっかりやっていこうという気持ちです。現状は未対応という感じです返信を追加ねこのいろいろやってます。Proよくある質問LinksX(Twitter)GitHubメディアキットLegal利用規約プライバシーポリシ。