Discord用通話開始・終了botを作った話。v13.x系対応バージョンをGlitchを使用して無料で作成

最近、友人と通話をする機会が多いのですが、今までDiscordに乗り換えるのが面倒だと一番大切な機能がない等の色々理由を付けてLINEのグループ通話機能を使っていました。

しかし、一度Discordの通話機能に慣れてしまうとLINE通話が不便で仕方ないので今度からDiscordで通話をしようという流れになったのですが、ここで一番困ったのが通話の開始・終了の通知がないことです。これが一番大切な機能で、それが欠けているからDiscordに移行しにくいという理由になっていました。

このグループでは誰かが通話を開始して待機し、暇な人が適当に参加するというスタイルをとっています。なので、最初に誰かが通話を始めたときにそれを周知する術がなければ手動でチャットを流すしかありません。たったこれだけのことなのですが、毎回この通知を人の手で行うのは面倒ですし、LINEの方では自動できていた機能なので周囲からの不満が大きかったです。

その不便な点を解消しようということで、色々調べ始めます。するとBotを入れればできるとか、自分でBotを作るとか色んな解決手段がでてきます。その中でなぜ自分でBotを作ることにしたのかという話と、現在ネットで公開される多くとはバージョンの関係でコードに互換がないので、Bot作成時点での最新版のv13.6.0で自分の求める機能を実装したという話です。

ちなみに、私はC言語やJavaを少し触れる程度しかないので、JavaScript手探りで実装した形になるので、実装が不適切な場所などがあるかと思います。それらはご指摘いただけると修正できますし、勉強にもなるので気づいたことがあればコメントでもGithubにでもよろしくお願いします。

目次

この記事で目指すこと・できること

はじめに、欲しい機能、この記事によって最終的にできるようになることを記します。この部分は同じような内容の他のサイトと似ているようで違う部分があるので、自分の求める内容であるかをここで確認してください。

私が欲しいと思った機能は非常にシンプルで箇条書きするとこんな感じです。

  • 特定のVCに誰もいない状態で通話を開始すると通知
    • 通知内容は誰が通話を始めたか
  • 特定のVCから誰もいなくなった際に終了の通知
    • こちらは終了したことだけを通知

私が求める機能はたったこれだけ。他のサイトだと誰かが入ったタイミングでそれを逐一通知するものだったりもします。ですが、求めるのはLINE通話と同等程度の機能で十分です。誰かが通話をしているがわかるだけでいいですし、無駄に通知しすぎると通知がたまってしまい寧ろ良くないと考えます。これらが私が当初目的としていたものです。

LINE通話のイメージ

次に、実際に実装を行ったものを示します。

  • 上記の当初の機能
  • 通話時間の表示

当初の目的に加えて通話時間の表示を実装しました、別に無くても困らないのですが、あったら面白いかと思い実装してみました。

ここで紹介しておくと、今回はGlitchというサービスなどを使いますが、基本的にはこちらのページを参考にして作成しました。

https://note.com/exteoi/n/nf1c37cb26c41

非常にわかりやすくありがたい記事となっていますが、少し前の話なので変更が必要な場合が結構あったのでこの記事をベースに話を進めます。

なぜこのような方法になったかをこれから説明していきます。不要なら読み飛ばしても良いのですが、そもそもこういう手段をとる必要がない可能性もあるので読んでから判断されたら良いかと思います。

実現方法を検討する

まず、求める機能の実装にはBotを使う他ないので、基本的にはどうBotを用意するかを考えました。

考えられる方法は以下の二つです。

  • 誰かが作ってくれたBotを利用する
  • 自分でサーバーを立ててBotを作る

基本的にはこの2種類しか実現のしようがないので、それぞれについて検討しました。

まず「誰かが作ってくれたBotを利用する方法」ですが、やはり同じようなことを考える人は多く検索では以下のような記事がヒットします。

https://note.com/ryu_sv/n/n88cb16edefc6

やっぱり同じことを考える人は多いんだなと思いとりあえず入れてみたものの、使いたいサーバーでは動作しませんでした。理由は不明ですが、サーバーを止めていたり、実装上の問題だったり色々あると思います。これが正しく動く環境なら今回のようにサーバーを立てたりする必要はないと思います。

不安定なのでは流石に任せられないなということで、信じられるのは自前のサーバーということで自分でサーバーを立てることにしました。

基本的には自分でサーバーを立てる場合は借りるか自宅に設置する必要があります。余談ですが、私の場合はこのWebサーバーを自宅に設置していた(現在はWebARENA Indigoに移行)ので設置は抵抗なくできます。幸運なことに使用するマシンも最近録画マシンからもWebサーバーとしての役目も終えたRaspberry Pi model 3B+があるので簡単に立てられます。

ですが、Discordのbotをいまいち理解していない点や、JavaScriptやnodeの動作をきっちり理解していないのに迂闊にサーバーを立てるのは危険性があるかもしれませんし、シンプルに手こずる可能性があります。後は家で動かすとなるとやはり電気代やマシンの寿命も付きものになります。

色々懸念材料がある中で調べまわっていると今回ベースとなる記事を発見。Glitchというサービスを使えば簡単なnodeアプリを無料で実行できる模様。多少制約があるみたいですが、これも工夫次第で何とかなるみたいです。これだ!ということで実際に実装をしてみたけど色々躓いたので、その解決方法と最新のコードを用いて実装をしたのが今回の話というわけです。

使い方等

先述しましたが、この記事を基本に設定を行います。

https://note.com/exteoi/n/nf1c37cb26c41

この通りにすれば問題のないところが多いです。トークンの取得などはそのまま行けます。ただ、古い内容のため、一部そのままではいけなかったり、仕様が変わっている部分があるのでその部分を説明しておきます。というか自分の備忘録も兼ねています。

では、どの部分が古い内容なのかというとGlitchのプロジェクトの作成部分です。プロジェクトを作成する部分で「hello-express」を選択するとのことですが、最新のバージョンではその項目がありません。おそらく一番近いのが「hello-node」なのですが、この状態では環境変数が設定できませんでした。もしかしたら私がよく理解していないせいなのもあるかもしれませんが、この記事にある「.env」というのがありません。

この.envというのを必要とする理由なのですが、資格情報など簡単に言えば秘密にしたい内容をここに記述することができるからです。これには他にも理由がありますが後述します。

さて、他のプロジェクトを開いてみても同じようなものが無かったのでなんとかならないかなぁと思っていたのですが、少し強引な方法で解決することにしました。Glitchはそもそも色んな人のコードを見ることができるのが売りでもあり、他人のレポジトリを簡単にコピーできます。そのため、これを活かして昔の「hello-express」と同様の構成を持つプロジェクトを自分のプロジェクトにコピーします。コピーするにはログインした状態で右上にある「Remix」ボタンを押せばプロジェクトをコピーできます。

https://glitch.com/edit/#!/important-chambray-liquid

これは適当に探して拾ってきた物になりますが、これを使って実装して結果的には問題ありませんでした。後は参考の記事の通りに実装をしていきます。

ここで、「.env」の環境ができれば欲しい理由を書いておきます。参考の記事では非公開に設定する項目についての記述があると思うのですが、Glitchは無料ユーザーの仕様が変わり、コードを非公開にする設定は有料ユーザーでないとできないようになりました。公開の状態で問題となるのはDiscordのトークンが見れてしまうことです。これを他人に知られてしまうとBotの乗っ取りが可能になってしまうので非公開にしたいわけです。コード全てを非公開にしなくても良い方法があり、それが環境変数です。環境変数に関しては以下のヘルプページに掲載されています。

https://help.glitch.com/kb/article/18-adding-private-data/

このページにあるようにプロジェクトをRemixしてコピーしても.envに含まれる変数を見ることはできません。外部から見ることもできません。なのでコード自体を公開しても問題なく使用できます。そのため「.env」ファイルを持つ構成で使用できればプロジェクトを非公開にしなくても問題ありません

さて、プロジェクトを作成したらプロジェクトをコピーします。かなりコードを変更しているので私のGitHubに別で用意しています。GitHubからインポートする際にこちらを指定してください。

T-H-Un/Discord_VC_Notifer_Base

こちらをインポートした後に環境変数を設定します。設定する必要があるのは以下の3つです。

項目
DISCORD_BOT_TOKENトークンの文字列
TEXT_CHANNEL_ID通知を送信したい
テキストチャンネルID
VOICE_CHANNEL_ID通話開始・終了判定を行う
ボイスチャンネルID
設定する環境変数

実際に設定するとこんな感じですね。不要かもしれませんが一応スクショを置いておきます。

設定画面

このように3つの変数を設定した状態であれば正しく機能すると思います。イマイチ関数の仕様がわかっていないのですが、もしかしたらメッセージを送信するテキストチャンネルでは一度は何かしらのメッセージを送らないと正しく動かないかもしれません。実際にBotを導入するとこんな感じです。

Botを使用してみた様子

後はこれを参考にした記事を基にGlitchのサーバーがスリープしないようにスクリプトを実行できるようにします。これは記事の通りで問題なく使えますが、トリガの実行間隔を1分にしました。

一応これで問題なく使えるのでこれでいいやって人はここまでで大丈夫です。他に機能追加したいとかであれば次の項でコードの解説と仕様を残しておくのでそちらをご覧ください。

コードの解説と仕様

コードの全容はこんな感じです。

const http = require('http');
const querystring = require('querystring');
const { Client, Intents } = require('discord.js');
const client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_VOICE_STATES] });

//叩き起こすためのサーバーを設置する make zombie server with google scripts
http.createServer(function(req, res){
  if (req.method == 'POST'){
    var data = "";
    req.on('data', function(chunk){
      data += chunk;
    });
    req.on('end', function(){
      if(!data){
        res.end("No post data");
        return;
      }
      var dataObject = querystring.parse(data);
      console.log("post:" + dataObject.type);
      if(dataObject.type == "wake"){
        console.log("Woke up in post");
        res.end();
        return;
      }
      res.end();
    });
  }
  else if (req.method == 'GET'){
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Discord Bot is active now\n');
  }
}).listen(3000);
 

//ボットが稼働状態になったら呼び出される。関数とステータスを設定している。 
//if Bot status is "ready", call this function. It7s start log and Set status of Bot.
client.on('ready', message =>{
  console.log('Bot_Ready');
  client.user.setActivity('Game', { type: 'PLAYING' });
});



/*通話用システム部分 for VC messages functions*/
//process.env.XXX みたいなのは全て.envファイルに正しく設定を行えている前提
//process.env.DISCORD_BOT_TOKEN -> Discord botのTOKENの文字列が格納されている
//process.env.TEXT_CHANNEL_ID -> channel IDの文字列が格納されている
//process.env.VOICE_CHANNEL_ID  -> ボイスチャットチャンネルの文字列が格納されている


var start_buf=Date.now();
var end_buf=Date.now();

client.on('voiceStateUpdate', (oldGuildMember, newGuildMember) =>{
  if(oldGuildMember.channelId==undefined&&newGuildMember.channelId!=undefined){
    if (newGuildMember.channelId == process.env.VOICE_CHANNEL_ID) {
      console.log("特定のボイスチャットチャンネルのみ反映");//OK
    if(client.channels.cache.get(newGuildMember.channelId).members.size==1){
    console.log("通話開始かどうかの条件判定");//OK
    let text="@everyone<@" + newGuildMember.id +"> が通話を開始しました。\n";
    client.channels.cache.get(process.env.TEXT_CHANNEL_ID).send(text);//このIDを送りたいチャンネルにする。1回は発言していないとおかしくなるかも。
    start_buf = Date.now();
      }
    }
  }
  
  if(newGuildMember.channelId==undefined&&oldGuildMember.channelId!=undefined){
    //console.log(client.channels.cache.get(oldGuildMember.channelId).members.size);
    console.log("通話終了の判定");//OK
    let text="通話が終了しました。\n";
    //newGuildMember
    if (oldGuildMember.channelId == process.env.VOICE_CHANNEL_ID) {
      console.log("特定のボイスチャンネル判定終わり");//OK
     if(client.channels.cache.get(oldGuildMember.channelId).members.size==0){
       console.log("最後の判定");
    client.channels.cache.get(process.env.TEXT_CHANNEL_ID).send(text);
    const end_buf = Date.now();
    const totaltime=end_buf-start_buf;
    const days=Math.floor(totaltime/1000/60/60/24);
    const hours=Math.floor(totaltime/1000/60/60)%24
    const min=Math.floor(totaltime/1000/60)%60;
    const sec = Math.floor(totaltime/1000)%60;
    let times="通話時間"+days+"日"+hours+"時間"+min+"分"+sec+"秒"
    client.channels.cache.get(process.env.TEXT_CHANNEL_ID).send(times);
    }
    }
    
  }
});

/*通話用システムここまで end VC function*/

//ログインする関数基本的にDiscord系の関数一番下において置く Login in on Discord with TOKEN
client.login( process.env.DISCORD_BOT_TOKEN );

このようなコードになっています。基本的にはコメントアウトである程度分かると思います。

//ボットが稼働状態になったら呼び出される。関数とステータスを設定している。 

このコメントアウトより上の行は参考記事のサーバーを立てるコードをそのまま拝借しています。それより下は自分で書いた部分になります。上記コメントアウトのすぐ下にあるこの部分はBotのステータスを設定しています。

client.on('ready', message =>{
  console.log('Bot_Ready');
  client.user.setActivity('Game', { type: 'PLAYING' });
});

Botの接続が確立されたときにBot_Readyとコンソールのログにでます。また、Discord側では「Gameをプレイ中」となると思います。Gameを変更すれば好きなゲーム名等に変更できます。

それ以降の部分は基本的に通話通知システムの部分です。まず以下の変数は通話時間のシステムのための外部の変数です。

var start_buf=Date.now();
var end_buf=Date.now();

ここが一番セオリーみたいなのがよくわかっていないので、適切な実装方法をご存じであれば教えていただきたいぐらいなのですが、これによって時間の情報を保持しています。これは基本的にいじらないでください。適当な値で初期化で良いですが、一応Date.now()で初期化しています。

client.on('voiceStateUpdate', (oldGuildMember, newGuildMember) =>{
  if(oldGuildMember.channelId==undefined&&newGuildMember.channelId!=undefined){
    if (newGuildMember.channelId == process.env.VOICE_CHANNEL_ID) {
      console.log("特定のボイスチャットチャンネルのみ反映");//OK
    if(client.channels.cache.get(newGuildMember.channelId).members.size==1){
    console.log("通話開始かどうかの条件判定");//OK
    let text="@everyone<@" + newGuildMember.id +"> が通話を開始しました。\n";
    client.channels.cache.get(process.env.TEXT_CHANNEL_ID).send(text);//このIDを送りたいチャンネルにする。1回は発言していないとおかしくなるかも。
    start_buf = Date.now();
      }
    }
  }

この部分が通話開始を判定している部分です。やっていることは以下の手順で判別しています。if文を追っていけばわかると思います。

  1. 同じVCに出入りした際のステータスの更新か
  2. 新しいステータスは指定したVCか
  3. 人数が0(undefined)から1になったか

この条件分岐を得て特定のVCに人が初めて入ったかを判定しています。そのあとにメッセージを生成して送っているという感じです。

その後にあるこれはその時のシステム時間を記録しています。

start_buf = Date.now();

これは終了時の時間を組み合わせるのでこの関数ではこれと言って使うわけではありません。

次にこの関数で終了を判定しています。

if(newGuildMember.channelId==undefined&&oldGuildMember.channelId!=undefined){
    console.log("通話終了の判定");//OK
    let text="通話が終了しました。\n";
    if (oldGuildMember.channelId == process.env.VOICE_CHANNEL_ID) {
      console.log("特定のボイスチャンネル判定終わり");//OK
     if(client.channels.cache.get(oldGuildMember.channelId).members.size==0){
       console.log("最後の判定");
    client.channels.cache.get(process.env.TEXT_CHANNEL_ID).send(text);
    const end_buf = Date.now();
    const totaltime=end_buf-start_buf;
    const days=Math.floor(totaltime/1000/60/60/24);
    const hours=Math.floor(totaltime/1000/60/60)%24
    const min=Math.floor(totaltime/1000/60)%60;
    const sec = Math.floor(totaltime/1000)%60;
    let times="通話時間"+days+"日"+hours+"時間"+min+"分"+sec+"秒"
    client.channels.cache.get(process.env.TEXT_CHANNEL_ID).send(times);
    }
    }
  }
});

判定は以下のようになっています。同様にif文を順番に追えばどういう実装をしているかがわかります。

  • 同じVCに出入りした際のステータス更新か
  • 新しいステータスは指定したVCか
  • 人数が1から0になったか

メッセージを送信した後に時間の計算作業をしていてそれがこの部分です。

   const end_buf = Date.now();
    const totaltime=end_buf-start_buf;
    const days=Math.floor(totaltime/1000/60/60/24);
    const hours=Math.floor(totaltime/1000/60/60)%24
    const min=Math.floor(totaltime/1000/60)%60;
    const sec = Math.floor(totaltime/1000)%60;
    let times="通話時間"+days+"日"+hours+"時間"+min+"分"+sec+"秒"
    client.channels.cache.get(process.env.TEXT_CHANNEL_ID).send(times);

ただ単にプログラム的に時間を求めているだけです。システム時間からシステム時間を引き算することで時間を求めています。もしかしたら時刻から求める方法もあるのかもしれませんが私は先述の方法を用いています。

このような方法で通話の開始と終了の通知を実現しています。やっていることは簡単なのですが、discord.jsの仕様変更がよくわからず色々苦戦しました。調べて出てくるのが昔の話ばかりなので調べたことがあっているかもわからない場合が殆どです。ドキュメントもわかりずらかったため、実装に苦労しました。特にvoiceStateUpdateで返ってくるnewStateとoldStateはわかりにくかったです。中の構造がイマイチ理解しにくかったので時間がかかってしまいました。

とりあえず実装はできましたが、私は冒頭に書いた通りC言語やjav aが多少かける程度なので実装が不適切な場合があるかと思いますが、それはご指摘いただければと思います。

まとめ

参考にした記事も内容が古く、それを自分でコードを書き工夫して使える形にしました。

自分に必要な機能のみを実装しネットにあるような不要な機能を持たせておりません。それ以外に、v13系対応の新しいコードは少なく公式ドキュメントもしょぼいので、実装は短いコードなのに苦労しました。

v13系では前例が少ないと思うので、最新の環境やアップデートで同じようなことを考えている方の役に立てば幸いです。

投稿日:
カテゴリー: TOOL

4件のコメント

  1. Glitch を使用したものではなく、node.jsで使えるコードはないでしょうか。
    プログラミング未経験です。
    よろしくお願いいたします。

    1. コメントありがとうございます。

      コードがあるかないかであればこのページの内容がほぼ答えなのが理解されていますか?
      まずは、Glitchで何をしているかを理解されるとよろしいかと思います。

      コード自体には丁寧にコメントアウトまで付けているのでどの部分が何をしているかぐらいはわかると思います。
      process.env.VOICE_CHANNEL_IDなどが対応する文字列を呼び出しているので、同様の部分を文字列に置き換えて関数が実行できる環境なら動かせると思います。

    1. コメントありがとうございます。

      私はその機能を使わないのでそのVCの招待リンク機能を知りません。もし、その招待リンクが固定であればそれを送ることは可能ですし。変化するものであっても、discord.jsの関数で得るができるのであれば可能だと思います。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です