Twitterのリンクを置き換えるDiscord Botを作りました。
Twitterのリンク ( https://twitter.com , https://x.com ) を自動的にFixTweetのリンク ( https://fxtwitter.com ) に置き換えるDiscord Botを作りました。おおよそ記事タイトルとこの一文だけで紹介が終わります。
導入はこちらからどうぞ: https://discord.com/oauth2/authorize?client_id=1163327448910401566&permissions=536882176&scope=bot
リポジトリはこちらです: https://github.com/Assault1892/discord-replace-twitter-link
とはいえこれだけだと味気なさすぎてアレなので説明できる内容があるかわかりませんが技術的解説とちょっとした開発備忘録でも。
おおまかな仕組み
ソース全体はこちらからどうぞ: https://github.com/Assault1892/discord-replace-twitter-link/blob/main/src/index.js
開発環境はWindows 10 22H2, Node.js v20.7.0, Discord.js 14.13.0です。
client.on(Events.MessageCreate) で全てのメッセージを広い、その中からmessage.content.match()と正規表現を用いてTwitterのリンクを引っ掛け、中にある twitter.com , x.com を fxtwitter.com に置き換えし、Webhookを用いて送信しています。わかる人ならこれだけでも同じのが作れそう。
正規表現での引っ掛け解説
message.content.match()で引っ掛けるのに使ってる正規表現パターンはこちらです。呪文みたいで楽しいですね。
初心者故のやらかし
元々はmessage.content.includes("https://twitter.com"|"https://x.com")を使っていましたが、これには問題があります。
多少のJS経験のある方なら分かると思いますが、これは「文中に1つの文字列を見つけることができたら反応する」というメソッドです。
String.prototype.includes() - JavaScript | MDN
これはつまりどういうことかというと、正規表現で引っかかった文字列...ではなく、含まれているアルファベットや数字が1個でも当てはまったら反応する、というものです。
これは流石によくない...というか、これだと誤反応まみれで終わりが始まります。1文字でも入っていたら終わりなので、一部の絵文字とかURLにも反応します。
流石にこれは許せないし、普通にうるさいので.includes()ではなく.match()と正規表現を使うようにしました。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/match
.match()は「正規表現のパターンにヒットしたら反応する」というものです*1。
単純に文字列内を走査して少しでも引っ掛けたら反応する.includes()と違い、確実に当たっていなければまず反応しないので、誤反応の確率をぐっと抑えられたと思います。というかこれで誤反応起こしたら泣く。
このあたりの助言をしてくれたフレンドには頭があがりません。本当にありがとうございます。
Webhookでの送信とスレッド対応
元々このBotはURLを置き換えした後、message.reply()を使って元メッセージに返信する形でURLの置き換えをするものでしたが、ログが流れやすい上、ログをさかのぼった際の視認性が低下するという弱点がありました。
流石にこんなのが何個もあったらうざいので、改善策を考えていたところ、別のフレンドから「Webhookを使ってメッセージを送信後、元メッセージを削除するのはどうか」と提案を受け、これを実装しました。記事トップ画像の左の奴です。
このあたりの実装にはDiscord.js Japan User GroupのScrapboxによって助けられました。というかほぼコード貰った感じです。本当にありがとうございます。
Webhookを使ってメッセージの発信者を任意のユーザーに見せかける - Discord.js Japan User Group
仕組みは上のScrapboxを見ていただければ分かると思いますが。
チャンネルのWebhookを取得し、存在しなければ作成し、それをmapに押し込めてキャッシュとして保持する感じです。
アイコンと表示名を元のメッセージのもの*2にし、contentにリンクを置換処理したメッセージを押し込んで送信することで、見かけ上は元のユーザーと一致してるように見せつつ*3リンクのみを置き換える、といった感じです。
スレッドで動かない
Webhookを使った方法には1個問題があります。チャンネル内スレッドで動作しません*4。
どうもスレッドではWebhookが使えない...というより、スレッドから親チャンネルのWebhookを取得できないみたい?よくわかりませんが、とにかくなんかだめっぽいです*5。
DIscord.js Docsなどを漁っていると別の方法を使えば行けそうな雰囲気もしますが、気力が持ちそうになく、また知識も情報も少ないため断念しました。
結果として、channel.message.isThreadがtrueの場合は通常通りmessage.channel.sendでメッセージを送信後 (返信ではない) 、元メッセージを削除する方針で実装をしました。
スレッド外ではWebhook、スレッド内ではメッセージ送信という処理分岐です。
なぜ返信でないのかというと、マジで原因不明ですが.reply()を使うとメッセージ削除でコケます。返信先メッセージが先に削除されてしまうのが原因っぽいんですが、それの改善方法が分からず、妥協案として.send()を使用しました。
正直なところ、いくらでも(改善|実装)方法はあっただろうに自分の力不足でそれをできなかったことが一番悔しいです。力をつけて再挑戦したい所。
コンソールのログが汚くなる問題
適当にconsole.log()とかでログを吐き出してるとよく起こるのが、「行頭揃ってなくて汚い問題」です。
これはお世辞にも見やすいとは言えません。何かあった際のチェックのためのログは判読性が重要です。
当初、console.logの頭に大量にスペースでも詰め込んでやろうと思いましたが、そうすると今度はソースの判読性が終わります。
どうしようかなあとしばらく考えた結果、行頭の空白の数を調整できるconsole.logもどきを作ることにしました。
ソースの最初らへんにあるalignedConsoleLogって奴がそれです。
実装自体は非常にシンプルで、第2引数に渡されたwidth分頭に空白を差し込み、その次に第1引数で渡されるmessageを置いてconsole.log()で吐き出すだけです。
非常にシンプルながらも割といい感じにできたほうなんじゃないかなあと思ってます。
大規模なプロジェクトになってくると流石にもうちょっと頭いい感じの実装をするか、あるいはそれ用のモジュールなどを使った方がいいと思います。*6
以上です。よければこのBotを使ってくれると嬉しいです。
フィードバックなどはサポートサーバーまたはTwitterのDMまでお願いします。
*1:もしかしたら正規表現である必要はなかったかもしれませんが、まぁ誤反応は起きて嬉しいものじゃないので...。
*2:message.author.avatarURLとmessege.member.displayNameで取得。
*3:Roleなどで色が変えられている場合は一致しません。WebhookではRoleの色などまで模倣できないです。仕様が変わっていなければ...。
*4:フォーラムでも動作しません。内部的にはスレッドの集まりです。
*5:実際にフォーラムのIntegrationsを見るとWebhookが作れることから、取得ができないだけっぽい感じはします。よくわからない。
*6:個人の趣味程度のプロジェクトなので、まぁこれぐらいでいいかなぁと妥協。