誰でも作れる!Discord Bot(応用編)

このnoteでは、誰でも作れる!Discord Bot(基礎編)で作ったBotに
様々な機能を実装する方法
を紹介します!
基礎編をまだ読んでいない方はそちらもどうぞ!

各セクションを読むだけで分かるように書いているつもりですが、応用編なのである程度プログラミングやDiscordの知識が必要かもしれません。まずは「Discordの基本知識」を読んでくださいね!

また、各セクションとても長い内容になっていますので、目次をご活用ください!

例として、takerun3367(たけるん!)さんが映っている画像が複数ありますが、この記事に関して彼はほとんど関係ないので、問い合わせなどをすることはお控えください。

ここに書いてある方法は、セキュリティ面では完璧ではないです。(Glitchのアプリケーション名とパラメーターが流出すると他の人でもメッセージを送信できてしまう)
セキュリティにて若干の対策は書いてありますが多分不完全なので、コピペしたものを実務等で使う際は自己責任でおねがいします。。。

Discordの基本知識

ここではDiscord Botを作るうえで役に立つ知識を書いていきます!Bot以外でも役に立つ内容もあるので是非是非読んでくださいね!

ID

一番大事な「ID」です!
これは、Discordのサーバー・チャンネル・ユーザー・絵文字等に与えられている固有の数字です。
これを取得するには、Discordの設定画面を開き、テーマから開発者モードを有効にする必要があります。

画像1

開発者モードにすると、チャンネルやユーザーなどを右クリックしたときに、「IDをコピー」という項目が表示されるようになります。

画像2


メンションする方法

例えば後述するmessage.reply( 'てきすと' )では、私の場合だと「@EOi, てきすと」と表示されます。これは前半のカンマまでの文章は固定になっており、不便です。そういう時はメンションを活用しましょう。

メンションには2種類あり、ユーザーへのメンションとテキストチャンネルへのメンションです。例えば、私のユーザーidが「236756208537567232」で、チャンネルのidが「725595164105768983」の場合、以下がメンションになります。

<@236756208537567232>
<#725595164105768983>

ユーザーの場合は「<@[ユーザーid]>」
チャンネルの場合は、「<#[チャンネルid]>」となります。@と#を間違えないように!

画像3

また、「<@&[ロールid]>」で役職に対してメンションすることができます。


バックスラッシュ(¥マーク)

上記のようなメンションは、バックスラッシュを使うことで内容が分かります。例えば、「@EOi」というメンションの前にバックスラッシュを入れた「\@EOi」をテキストチャンネルで発言すると、「<@236756208537567232>」のような表示になります。

画像4

絵文字のidもこれで取得できます。


書式

書式を活用すると、文章を装飾することができます。

画像5

これについては、Discordの公式ブログにて紹介されているのでそちらをお読みください。


embed(埋め込みメッセージ)

Discordにはembedという埋め込みメッセージがあります。例えば、youtubeのリンクを貼った時にメッセージの下に出てくるやつです。メッセージを送信するでも紹介されています。

画像6

これはBotから表示させることができます。設定によっては表示されないので、表示されないときは設定→テキスト→リンクプレビューを有効にしましょう。

画像7


~discord.jsの機能紹介~

ここからはdiscord.jsの機能を、具体的な実装を例に紹介していきます。それぞれ目次から飛べるようになっています!

GitHubに、ここで紹介しているものをまとめています!
https://github.com/ExtEOi/DiscordBotBase/tree/Advanced


メッセージを送信する

メッセージを送信するには、チャンネルのsendメソッドを使います。sendMsg()にて使われています。使い方がよくわからなければ、この関数を使いましょう!

function sendMsg(channelId, text, option={}){
  client.channels.get(channelId).send(text, option)
    .then(console.log("メッセージ送信: " + text + JSON.stringify(option)))
    .catch(console.error);
}

また、先ほど紹介した通りembedという埋め込みメッセージがあります。これはこのようにして送信します。

let msgChannelId = 722495970700689429;
let emb = {embed: {
  author: {
    name: "takerun3367",
    url: "https://www.youtube.com/channel/UCyM2Qcy6iD43d8BgiPj3ClQ",
    icon_url: "https://yt3.ggpht.com/a/AATXAJzj95tFkxDHHJ2FMMzMkO0AOI0Tk-Zb4Ld0mw=s100-c-k-c0xffffffff-no-rj-mo"
  },
  title: "【maimai外部出力(60fps)】CHAOS DXMAS AP",
  url: "https://www.youtube.com/watch?v=N_y9OTC17MU",
  description: "タッチノーツの使い方を模索しようという意図が見て取れますね、無限の可能性を感じます",
  color: 7506394,
  timestamp: new Date(),
  thumbnail: {
    url: "http://img.youtube.com/vi/N_y9OTC17MU/mqdefault.jpg"
  }
}};
sendMsg(msgChannelId, "<@270557414510690305> の新着動画!", emb);

画像11

embedはこのサイトでテストすることができます。
詳しくはググってね。。。

また、discord.jsにはこのembedを簡単に作るRichEmbedというものがあります。Documentationに詳細が書かれていたり、下記の「投票コマンドを作る」にて使われているので、興味があればぜひ!

const discord = require('discord.js');
let emb = new discord.RichEmbed();


メッセージが送信されたときの動作を決める

Botが見れるチャンネルにメッセージが送信されたときに、「message」イベントが発火します。client.on()でイベント発火時の動作を決めることができます。

下記のコードでは、自分自身のメッセージやBotからのメッセージを無視する、というものになっています。「message」イベントの部分では、基本的に最初に入れましょう。

client.on('message', message =>{
  if (message.author.id == client.user.id || message.author.bot){
    return;
  }
});


ある言葉が含まれるメッセージにリアクションする

画像8

この例では、「☆5」「星5」のような文字列や、☆5を表す絵文字が含まれているメッセージに、☆5の絵文字をリアクションしています。

client.on('message', message =>{
  if (message.author.id == client.user.id || message.author.bot){
    return;
  }

  if (message.content.match(/[★☆星][55]|<:5star:723422237973151776>/)) {
    let react = message.guild.emojis.get('723422237973151776');
    message.react(react)
      .then(message => console.log("リアクション: <:5star:723422237973151776>"))
      .catch(console.error);
  }
});

絵文字は、上記のバックスラッシュにて紹介されている方法で文字列を得ることができます。今回は、サーバー絵文字なので「<:5star:723422237973151776>」という表記になっています。

ある言葉が含まれるかどうかは、message.content.match()を使います。
括弧の中に正規表現を入れれば含まれるかどうかを判定してくれます。
正規表現はググってね~

リアクションをするには、メッセージのreactメソッドを使います。引数には絵文字を指定しますが、サーバー絵文字を指定する際はmessage.guild.emojis.get('絵文字id')を用います。
標準絵文字は上記バックスラッシュで得た絵文字を直接渡します。

client.on('message', message =>{
  if (message.author.id == client.user.id || message.author.bot){
    return;
  }

  if (message.content.match(/[★☆星][55]|<:5star:723422237973151776>/)) {
    let react = message.guild.emojis.get('723422237973151776');
    message.react(react)
      .then(message => console.log("リアクション: <:5star:723422237973151776>"))
      .catch(console.error);
  }

  //ぴえんの例
  if (message.content.match(/🥺/)) {
    let react = '🥺';
    message.react(react)
      .then(message => console.log("リアクション: 🥺"))
      .catch(console.error);
  }
});

この例だと、「☆5🥺」というメッセージには両方の絵文字でリアクションすることになります。ここの条件を変えたければif文を工夫したり、return;を使ったりしてくださいね!

特定のキーワードに反応する

画像9

client.on('message', message =>{
  if (message.author.id == client.user.id || message.author.bot){
    return;
  }

  if (message.content === "にゃ~ん"){
    let reply_text = "にゃ~ん";
    message.reply(reply_text)
      .then(message => console.log("Sent message: " + reply_text))
      .catch(console.error);
    return;
  }
});

特定のキーワードに反応させるには

if (message.content === "にゃ~ん"){}

のようにします。これは「にゃにゃにゃにゃ~ん」等には反応せず、「にゃ~ん」のみに反応します。


おみくじコマンド

画像12

client.on('message', message =>{
  if (message.author.id == client.user.id || message.author.bot){
    return;
  }
  if (message.content.match(/^!おみくじ/) ||
      (message.isMemberMentioned(client.user) && message.content.match(/おみくじ/))){
    let arr = ["大吉", "吉", "凶", "ぽてと", "にゃ~ん", "しゅうまい君"];
    lottery(message.channel.id, arr);
  }else if (message.isMemberMentioned(client.user)) {
    sendReply(message, "呼びましたか?");
  }
});

function lottery(channelId, arr){
  let random = Math.floor( Math.random() * arr.length);
  sendMsg(channelId, arr[random]);
}

この例では、「!おみくじ」で始まる文章、もしくはbotのメンションに「おみくじ」という言葉が含まれているとき、おみくじをしてくれます。
この例では、配列に含まれている要素を、ランダムに返します。

大吉5%、吉30%...としたい場合は、↓のようにします。

client.on('message', message =>{
  if (message.author.id == client.user.id || message.author.bot){
    return;
  }
  if (message.content.match(/^!おみくじ/) ||
      (message.isMemberMentioned(client.user) && message.content.match(/おみくじ/))){
    let arr = ["大吉", "吉", "凶", "ぽてと", "にゃ~ん", "しゅうまい君"];
    let weight = [5, 30, 10, 15, 20, 20];
    lotteryByWeight(message.channel.id, arr, weight);
  }else if (message.isMemberMentioned(client.user)){
    sendReply(message, "呼びましたか?");
  }
});

function lotteryByWeight(channelId, arr, weight){
  let totalWeight = 0;
  for (var i = 0; i < weight.length; i++){
    totalWeight += weight[i];
  }
  let random = Math.floor(Math.random() * totalWeight);
  for (var i = 0; i < weight.length; i++){
    if (random < weight[i]){
      sendMsg(channelId, arr[i]);
      return;
    }else{
      random -= weight[i];
    }
  }
  console.log("lottery error");
}

これは、let weightの部分で確率を設定しています。分かりやすく合計値が100になるようにしていますが、その必要はなく、単に要素間の比率を設定すれば問題ないです。


誰かが通話を始めたときにお知らせする

画像10

const mainChannelId = [テキストチャンネルid];

client.on('voiceStateUpdate', (oldGuildMember, newGuildMember) =>{
 if(oldGuildMember.voiceChannelID === undefined && newGuildMember.voiceChannelID !== undefined){
   if(client.channels.get(newGuildMember.voiceChannelID).members.size == 1){
     if (newGuildMember.voiceChannelID == 725595164105768984) {
       newGuildMember.voiceChannel.createInvite({"maxAge":"0"})
         .then(invite => sendMsg(
           mainChannelId, "<@" + newGuildMember.user.id +"> が通話を開始しました!\n" + invite.url
         ));
     }
   }
 }
});

通話状態の変更は「voiceStateUpdate」イベントで検知できます。
この例ではお知らせ先のテキストチャンネルをidで設定しているので、お知らせ先は固定となります。コピペする際は変更するのをお忘れなく!


if(oldGuildMember.voiceChannelID === undefined && newGuildMember.voiceChannelID !== undefined){}

これは、変更前の通話状態にボイスチャンネルのidが含まれておらず、変更後の通話状態にボイスチャンネルのidが含まれている場合を検知するif文です。これは、どのボイスチャンネルにも入っていない状態から、ボイスチャンネルに入った時、ということになります。

2022/06/19 追記
上手く動かない際は、「undefined」を「null」にすると動く可能性があります。


if(client.channels.get(newGuildMember.voiceChannelID).members.size == 1){}

次のif文では、入った先のボイスチャンネルの人数が1人である場合を検知します。これは、誰も入っていないボイスチャンネルに入った時ということになります。


if (newGuildMember.voiceChannelID == 725595164105768984){}

その次では、ボイスチャンネルのidをチェックしています。
ボイスチャンネルが複数ある中で、idが「725595164105768984」のボイスチャンネルのみお知らせしたいからです。

人数チェックやidチェックは、不要な方は消してください。


if(oldGuildMember.voiceChannelID !== undefined && newGuildMember.voiceChannelID === undefined){}

上のようにすると、ボイスチャンネルから切断したとき、となります。


if(oldGuildMember.voiceChannelID != newGuildMember.voiceChannelID){}

このようにすると、ボイスチャンネルを移動したとき、になるはず。(未検証、undefinedかどうかのチェックが必要です。)


Youtubeチャンネルの新規投稿をお知らせする


6/28修正 IFTTTからのPOSTを、GlitchがBANしていたため、修正しました。


画像13

これは、先ほども例に挙がっていたこれです。

紹介しているコードでは汎用性があまりなく、複数の投稿者についてお知らせしたい場合、少し書いてある方法と変える必要があります。IFTTTのPOSTデータに、authorで設定するチャンネル名やURLを含めると上手くいくと思います。

最初に、Google Apps ScriptでPOSTを受け取るための関数を作ります。
公開→ウェブアプリケーションとして導入 からURLを発行します。

画像27

このように選ぶとURLが生成されます。doPost関数を作ればGAS側はOK。

function doPost(e) {
 var PostData = JSON.parse(e.postData.contents);
 sendGlitch(GLITCH_URL, PostData);
}


次にIFTTTに登録します。https://ifttt.com/
登録出来たら、このページからアプレットを作成します。https://ifttt.com/create
「+This」と書かれているところを押して、Youtubeを選択し、「New public video from subscriptions」を押してお知らせしたいチャンネルを押します。
そのチャンネルを登録していることと、チャンネルを登録しているYoutubeアカウントと連携することが必要になります。

画像14

「+That」ではWebhooksを選択します。

画像15

URLに上記のGoogle Apps ScriptのWebアプリケーションURLを入力し、「POST」と「application/json」を選択。Bodyには

{"type":"newTakerunVideo", "debug":"false", "title":"{{Title}}", "description":"{{Description}}", "url":"{{Url}}" }

のように入力します。newTakerunVideoのところは例なので、各自好きなものに変えてください。
※画像にはdebugが入っていませんが、入れないとデバッグ用チャンネルにお知らせされてしまうので注意してください。

https://ifttt.com/my_appletsの画面がこのようになっていればOKです。

画像16

後は、Glitchのserver.jsでがんばります。
※6/28 dataObject.url.replace("https://youtu.be/", "");に修正。

const debugChannelId = "722495970700689429";
const mainChannelId = "723174199367041077";

http.createServer(function(req, res){
  if (req.method == 'POST') {
    var data = "";
    req.on('data', function(chunk) {
        data += chunk;
    });
    req.on('end', function() {
      if(!data){
        console.log("No post data");
        res.end();
        return;
      }
      var dataObject = querystring.parse(data);
      console.log("post:" + dataObject.type);
      if(dataObject.type == "wake"){
        console.log("Woke up in post");
        res.end();
        return;
      }
      if(dataObject.type == "newTakerunVideo"){
        let msgChannelId = debugChannelId;
        if(dataObject.debug !== undefined && dataObject.debug == "false"){
          msgChannelId = mainChannelId;
        }
        let msgMention = "<@270557414510690305>";
        let videoId = dataObject.url.replace("https://youtu.be/", "");
        let emb = {embed: {
          author: {
            name: "takerun3367",
            url: "https://www.youtube.com/channel/UCyM2Qcy6iD43d8BgiPj3ClQ",
            icon_url: "https://yt3.ggpht.com/a/AATXAJzj95tFkxDHHJ2FMMzMkO0AOI0Tk-Zb4Ld0mw=s100-c-k-c0xffffffff-no-rj-mo"
          },
          title: dataObject.title,
          url: dataObject.url,
          description: dataObject.description,
          color: 7506394,
          timestamp: new Date(),
          thumbnail: {
            url: "http://img.youtube.com/vi/" + videoId + "/mqdefault.jpg"
          }
        }};
        sendMsg(msgChannelId, msgMention + " の新着動画!", emb);
      }
      res.end();
    });
  }else if (req.method == 'GET') {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Discord bot is active now \n');
  }
}).listen(3000);

まず、Botのデバッグ用のチャンネルと、実際に投稿するチャンネルのidを「debugChannelId」と「mainChannelId」に代入しておいてください。
次にif(dataObject.type == "newTakerunVideo"){}のところを、実際にIFTTT側で設定した"type"の文字列に変えてください。
後は、embedの中のauthorに投稿者情報を追加すれば完成です。

うまくできているかのテストとして、Google Apps ScriptからPOSTしてみる、という方法があります。以下のようなテスト用のスクリプトを作ってみましょう。

画像17

function postTest(){
  var json = {
    "type":"newTakerunVideo",
    "debug":"true",
    "title":"【maimai外部出力(60fps)】CHAOS DXMAS AP",
    "description":"タッチノーツの使い方を模索しようという意図が見て取れますね、無限の可能性を感じます",
    "url":"https://www.youtube.com/watch?v=N_y9OTC17MU"
  };
  sendGlitch(GLITCH_URL, json);
}

この関数を実行すると、Glitchのログに「post:newTakerunVideo」と出てきて、デバッグ用チャンネルにお知らせメッセージが送信されると思います。
debugをfalseにすると、mainChanneiIdで設定したチャンネルに送信されます。

IFTTTにはこのような機能がたくさんあり、WebhooksでGlitchに送るだけで何でも受け取ることができるので、試してみると面白いですよ!


スプレッドシート連携

このセクションはとても長い上に、プログラムは例で紹介されている用途専用になっています。必要なところだけをいい感じに使ってくださいね!
また、ここで紹介されているプログラムはものすごーく適当な部分があったりするので、あまりあてにしないでください。

画像18

ここでは、スプレッドシートを利用する方法を紹介していきます!
この例では、maimaiという音楽ゲームのスコアを書いたExcelシートからスコアデータを取得し、更新があれば通知をしています!

このBotは、「maimaiのでらっくスコアに興味のある人」向けのDiscordサーバー、「でらスコPortal」にて運用されています。maimaiをプレイしていて興味のある方はたけるん!氏のTwitterからどうぞ!

画像19

実際のスコアシートです。これはOneDriveに保存されています。

ここからはGoogle Apps Scriptを使います。下準備として、Drive APIを有効にしましょう。リソース→Googleの拡張サービスからDrive APIを有効にします。

画像21

画像22


まずは実行することになるメインの関数を紹介します。ここから先紹介される関数を、この関数の下に並べていけば動くようになります。

var excelFolderId = "13VElyU1xxxz20Ubh9xxxxxxxxxxxxxxx";
var destFolderId  = "1YclJucmxxxNlq1-Ysxxxxxxxxxxxxxxx";
var source_folder = DriveApp.getFolderById(excelFolderId);
var dest_folder   = DriveApp.getFolderById(destFolderId);
var scoreSheetArray = ["POPS&アニメ", "niconico&ボーカロイド", "東方Project", "ゲーム&バラエティ", "maimai", "オンゲキ&CHUNITHM", "ReMASTER"];

function scoreUpdate(){
  var oldFileName = "DSPA_orig_old";
  var newFileName = "DSPA_orig_new";
  var excelUrl = "https://docs.google.com/spreadsheets/d/1ccr1yJiRtrR-gnmcvxxxxxxxxxxxxxxxxxxxxxx/export?format=xlsx";
  arrangeSheet(oldFileName, newFileName);
  createSheet(excelUrl, newFileName);
  var oldFileId = DriveApp.getFolderById(destFolderId).getFilesByName(oldFileName).next().getId();
  var newFileId = DriveApp.getFolderById(destFolderId).getFilesByName(newFileName).next().getId();
  sortSheetData(oldFileId, newFileId);
  var scoreData = compareSheetData(oldFileId, newFileId);
  postScoreUpdate(scoreData);
}

まず、最初に宣言されているFolderIdですが、これはGoogleDriveにアクセスすることで確認することができます。この例では、ダウンロードしたExcelを一時保存する用のフォルダと、スプレッドシートを保管しておくフォルダを用意しています。

画像20

urlがhttps://drive.google.com/drive/u/0/folders/13VElyU1xxxz20Ubh9xxxxxxxxxxxxxxx
だった場合、FolderIdは「13VElyU1xxxz20Ubh9xxxxxxxxxxxxxxx」となります。
excelUrlには、ExcelファイルのあるURLを指定します。スプレッドシートの場合は、urlの「edit」以降を「export?format=xlsx」に置き換えます。

https://docs.google.com/spreadsheets/d/1JBygfrxTYPiI5AlX2ra4Z0GPUXI35M-H1xxxxxxxxxx/edit#gid=0

https://docs.google.com/spreadsheets/d/1JBygfrxTYPiI5AlX2ra4Z0GPUXI35M-H1xxxxxxxxxx/export?format=xlsx


1. Excelファイルを取得

最初にExcelファイルを取得します。この例ではスコアの更新を検知するために、Excelファイルを取得した後保存しておき、再度Excelファイルを取得してからその2つのファイルを比較しています。
これを実装するためにGoogleDriveを利用します。

function arrangeSheet(oldFileName, newFileName){
  if(DriveApp.getFolderById(destFolderId).getFilesByName(oldFileName).hasNext()){
    DriveApp.getFolderById(destFolderId).getFilesByName(oldFileName).next().setTrashed(true);
  }
  if(DriveApp.getFolderById(destFolderId).getFilesByName(newFileName).hasNext()){
    DriveApp.getFolderById(destFolderId).getFilesByName(newFileName).next().setName(oldFileName);
  }
}

function createSheet(excelUrl, sheetName){
  source_folder.createFile(getExcelBlob(excelUrl));
  var excel_files = source_folder.getFiles();
  if(excel_files.hasNext()) {
    var file = excel_files.next();
    convertToSpreadsheet(file, dest_folder, sheetName);
    file.setTrashed(true);
  }
}

function getExcelBlob(url) {
  var blob = UrlFetchApp.fetch(url).getBlob();
  return blob;
}

function convertToSpreadsheet(file, folder, sheetName) {
  options = {
    title: sheetName,
    mimeType: MimeType.GOOGLE_SHEETS,
    parents: [{id: folder.getId()}]
  };
  Drive.Files.insert(options, file.getBlob());
}

Excelファイルを取得する前に、arrangeSheet関数で元々あるスプレッドシートの整理をします。過去のスプレッドがあればゴミ箱に入れ、前回取得したスプレッドシートの名前を、「old」のような名前をつけています。
その後、createSheet関数でExcelファイルをダウンロードし、「new」のような名前を付けます。


2. スプレッドシートをいじる

スプレッドシートを触っていきます。
まずはsortSheetData関数でスプレッドシートをソートしていきます。
スプレッドシート内のシートは、getSheetByNameメソッドで取得することができます。
ここでは曲名でソートしています。上記にあるように、G列に曲名が書いてあるので、newSheet.sort(7);で左から7列目基準でソートしています。

次にcompareSheetData関数でスコアデータを比較していきます。スプレッドシートのデータは、newSheet.getRange("A:P").getValues();のようにすることで配列として得ることができます。ここではA列からP列のすべてを取得していますが、特定の範囲のみを取得することもできます。注意点として、先ほどのソートでは曲名は7列目ということで7を引数にしていましたが、配列の場合の添字は6になります。当たり前ですが。。。
とにかくわちゃわちゃ調べて、更新があった場合にその曲の曲名やスコアやスコアの上限などを配列に入れていき、調べ終わったら配列を返すという関数になっています。

function sortSheetData(oldId, newId){
  var oldSS = SpreadsheetApp.openById(oldId);
  var newSS = SpreadsheetApp.openById(newId);
  for(var i = 0; i < scoreSheetArray.length; i++){
    var oldSheet = oldSS.getSheetByName(scoreSheetArray[i]);
    var newSheet = newSS.getSheetByName(scoreSheetArray[i]);
    oldSheet.sort(7);
    newSheet.sort(7);
  }
}

function compareSheetData(oldId, newId){
  var dataArray = [];
  var oldSS = SpreadsheetApp.openById(oldId);
  var newSS = SpreadsheetApp.openById(newId);
  for(var i = 0; i < scoreSheetArray.length; i++){
    var oldSheet = oldSS.getSheetByName(scoreSheetArray[i]);
    var newSheet = newSS.getSheetByName(scoreSheetArray[i]);
    var oldValues = oldSheet.getRange("A:P").getValues();
    var newValues = newSheet.getRange("A:P").getValues();
    var offset = 0;
    for(var j = 0;j < oldValues.length;j++){
      var oldSongName = oldValues[j][6];
      var newSongName = newValues[j+offset][6];
      var oldTeScore = oldValues[j][4]
      var oldTaScore = oldValues[j][5]
      var newTeScore = newValues[j+offset][4];
      var newTaScore = newValues[j+offset][5];
      if(oldTeScore == ""){oldTeScore = 0;}
      if(oldTaScore == ""){oldTaScore = 0;}
      if(newTeScore == ""){newTeScore = 0;}
      if(newTaScore == ""){newTaScore = 0;}
      if(newValues[j][15] == ""){
        newValues[j][15] = newValues[j][14];
      }
      if(oldValues[j][15] == ""){
        oldValues[j][15] = oldValues[j][14];
      }
      var newScoreMax = newValues[j+offset][15];
      var oldScoreMax = oldValues[j][15];
      if(newSongName == "" || newSongName == "name") continue;
      if(oldSongName != newSongName){
        dataArray.push([newSongName, newScoreMax, 0, 0, newTeScore, newTaScore]);
        offset++;
        j--;
        continue;
      }
      if(oldTeScore != newTeScore || oldTaScore != newTaScore){
        dataArray.push([newSongName, newScoreMax, oldTeScore, oldTaScore, newTeScore, newTaScore]);
      }
    }
  }
  return dataArray;
}


3. Glitchに送信し、Botにお知らせしてもらう

後は、Youtubeの時のように、Glitchに送信するだけです。2. で得られた配列を引数として、embedの文章を気合で作っていきます。
ここのコードはほんとに適当なのでお察しください。。。
ここで使っている小技として、Discordの絵文字は大きさが謎なので、透過pngで作った完全に透明な絵文字を入れることで、片方だけにトロフィーを表示しても表示が崩れないようになっています。

function postScoreUpdate(data){
  var array = [];
  for(var i = 0; i < data.length; i++){
    var te = "";
    var ta = "";
    if(data[i][4] > data[i][5]){
      te = ":trophy: ";
      ta = "<:blank:722859508204044390> ";
    }else if(data[i][4] < data[i][5]){
      te = "<:blank:722859508204044390> ";
      ta = ":trophy: ";
    }else{
      te = ":trophy: ";
      ta = ":trophy: ";
    }
    var nowtediff = (data[i][4] - data[i][1] == 0)?":star:":data[i][4] - data[i][1];
    te += "て: __" + data[i][4] + "__ (" + nowtediff + ")";
    if(data[i][2] != data[i][4]){ te += " ← " + data[i][2] + " (" + (data[i][2] - data[i][1]) + ")";}
    var nowtadiff = (data[i][5] - data[i][1] == 0)?":star:":data[i][5] - data[i][1];
    ta += "た: __" + data[i][5] + "__ (" + nowtadiff + ")";
    if(data[i][3] != data[i][5]){ ta += " ← " + data[i][3] + " (" + (data[i][3] - data[i][1]) + ")";}
    array.push({
      "name": data[i][0] + " (" + data[i][1] + ")",
      "value": te + "\n" + ta + "\n",
      "inline": "False"
    });
  }
  if(array.length > 0){
    var emb = {
      "description": ":fire:「[でらっくスコア大戦](https://1drv.ms/x/xxxxxxxxxxxxxxxxxxxxxxxxx)」更新情報 :fire:",
      "fields": array,
      color: 7506394
    };
    var json = {
      'type':'announce',
      'debug':'false',
      'content': JSON.stringify(emb)
    };
    sendGlitch(GLITCH_URL, json);
  }
}

ここまでできたら後はGlitch側にちょっと追加するだけ。

http.createServer(function(req, res){
  if (req.method == 'POST'){
    var data = "";
    req.on('data', function(chunk){
      data += chunk;
    });
    req.on('end', function(){
      if(!data){
        console.log("No post data");
        res.end();
        return;
      }
      var dataObject = querystring.parse(data);
      console.log("post:" + dataObject.type);
      if(dataObject.type == "wake"){
        console.log("Woke up in post");
        res.end();
        return;
      }

      //ここから
      if(dataObject.type == "announce"){
        let msgChannelId = debugChannelId;
        if(dataObject.debug !== undefined && dataObject.debug == "false"){
          msgChannelId = mainChannelId;
        }
        let emb = {embed: JSON.parse(dataObject.content)};
        sendMsg(msgChannelId, "", emb);
      }
      //ここまで

      res.end();
    });
  }
  else if (req.method == 'GET'){
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Discord Bot is active now\n');
  }
}).listen(3000);

お疲れさまでした。(サーバーが違ったりデータが違うのでバグってます)

画像23

これを、基礎編でも紹介しているトリガーで好きな間隔で実行すれば定期的にお知らせしてくれるようになります。

※例えばDiscord側でエラーが発生してメッセージを送信できなかった場合に、res.end();で失敗ステータスをレスポンスしてファイルをロールバックするなど、もう少しきっちり作ることもできますが、そこまで真面目なBotではないので今回は省略。


セキュリティ

ここまでで紹介してきたプログラムは、セキュリティ的にはちょっと甘い(と思う)ので、結構真面目な場所でBotを使いたいときは、以下の改良をするといいかもしれません。簡潔に言うと、ハッシュを使って認証をします。セキュリティの専門家ではないので、手法が古いかもしれませんがご了承ください。

まず、Glitch側でcryptoパッケージをインストールします。

const crypto = require('crypto');

後はこんな感じ。ハッシュとノンスを受け取って検証します。

const password = "potatoisgodpotatoisgodpotatoisgod";

http.createServer(function(req, res){
  if (req.method == 'POST'){
    var data = "";
    req.on('data', function(chunk){
      data += chunk;
    });
    req.on('end', function(){
      if(!data){
        console.log("No post data");
        res.end();
        return;
      }
      var dataObject = querystring.parse(data);
      console.log("post:" + dataObject.type);
      if(dataObject.type == "wake"){
        console.log("Woke up in post");
        res.end();
        return;
      }

      //ここから
      if(dataObject.hash === undefined || dataObject.nonce === undefined){
        console.log("undefined hash");
        res.end();
        return;
      }else{
        let serverHash = crypto.createHash('sha256').update(password + Math.floor(dataObject.nonce)).digest('hex');
        if(String(dataObject.hash) != serverHash){
          console.log("invalid hash");
          res.end();
          return;
        }else{
          console.log("nonce:" + Math.floor(dataObject.nonce));
          console.log("hash ok");
        }
      }
      //ここまで

      if(dataObject.type == "announce"){
        let msgChannelId = debugChannelId;
        if(dataObject.debug !== undefined && dataObject.debug == "false"){
          msgChannelId = mainChannelId;
        }
        let emb = {embed: JSON.parse(dataObject.content)};
        sendMsg(msgChannelId, "", emb);
      }
      res.end();
    });
  }
  else if (req.method == 'GET'){
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Discord Bot is active now\n');
  }
}).listen(3000);


Google Apps Scriptでノンスを生成し、ハッシュを生成してPOST時に渡します。

var password = "potatoisgodpotatoisgodpotatoisgod";
function postTest() {
  var nonce = Math.floor(Math.random()*100000000);
  var hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, password+nonce)
               .reduce(function(str,chr){
                 chr = (chr < 0 ? chr + 256 : chr).toString(16);
                 return str + (chr.length==1?'0':'') + chr;
               },'');
  var json = {
    "type":"newTakerunVideo",
    "hash":hash, 
    "nonce":nonce,
    "debug":"true",
    "title":"【maimai外部出力(60fps)】CHAOS DXMAS AP",
    "description":"タッチノーツの使い方を模索しようという意図が見て取れますね、無限の可能性を感じます",
    "url":"https://www.youtube.com/watch?v=N_y9OTC17MU"
  };
  sendGlitch(GLITCH_URL, json);
}

こうすることである程度セキュリティが向上します。これでも盗聴されていたりすると意味が無いですが、何もないよりはマシかもしれません。

セッションごとにサーバー側から新しいノンスを発行し、クライアント側に渡すというのが正しい方法ですが、実装が大変すぎるのでやりません。。。


対話型Botを作る / 投票コマンド実装

画像24

画像25

画像26

コマンドを実行したときに、詳細をBotが聞いてくれるようなBotの作り方です。ここでは、投票を作るコマンドの実装を例に紹介していこうと思います!

discord.js-commandoという便利なパッケージがあるので、インストールしておいてくださいね。
大規模・本格的なBotを作る際にはこのような手法をよく使うので、プログラミングに慣れている方はこちらも使ってみてくださいね!

上記のBotに追加していくことも可能ですが、とても分かりにくいものになってしまうので、ここでは新しくBotアカウントから作っています。

全てのコードはGithubに載せています!
https://github.com/ExtEOi/DiscordBotBase/tree/vote

詳細は見たらある程度分かるはずなので、細かいポイントを解説していきます。

まずはRichEmbed。embedをめちゃくちゃ簡単に作ることができます。これを今まで使っていなかったのは、単に知らなかったからです。。。
上記のsendMsg()でも使うこともできるので便利!

var emb = new discord.RichEmbed()
           .setTitle(question)
           .setDescription(detail)
           .setAuthor(msg.author.username, msg.author.displayAvatarURL)
           .setColor(0x7289DA)
           .setTimestamp();
emb.setFooter('この投票は無期限です。');


次に対話の内容について。これはvote.jsのVoteクラスのコンストラクタの、argsの書き方によって変わります。

args: [
    {
        key: 'question',
        prompt: '投票のテーマを入力してください。',
        type: 'string',
        validate: question => {
            if (question.length < 31 && question.length > 4) return true;
            return 'テーマは5~30文字にしてください。';
        }
    },
    {
        key: 'detail',
        prompt: '詳細を入力してください。',
        type: 'string',
        validate: desc => {
            if (desc.length < 201 && desc.length > 0) return true;
            return '詳細は1~200文字にしてください。';
        }
    },
    {
        key: 'time',
        prompt: '投票を受け付ける時間を入力してください(分)。0を指定すると制限時間無しになります。',
        type: 'integer',
        validate: time => {
            if (time >= 0 && time <= 60) return true;
            return '時間は0~60分にしてください。';
        } 
    },
    {
        key: 'channel',
        prompt: '投票をしたいチャンネルを入力してください。',
        type: 'channel'
    }
]

前提として、このvoteコマンドは

!vote "ネコ派の人~~~!" "ネコ派の人は✅ 、イヌ派の人は❌ 、それ以外の人は🤔 を選んでね~" 0 #一般

と打てば一発で投票を作成してくれます。「!vote」のように、必要事項が不足している場合に何を入れるかを聞いてくれる、というものになっています。例えば、Botに聞かれるまでもないけどオプションとして用意しておきたいという場合には、defaultプロパティで規定値を設定しておくことで、書いていなくても聞かれないけど設定できる、という風になります。

選択肢1. 2. 3.....に答えてもらいたいときは、数字の絵文字を使うことで出来ます。

var emojis = ['1⃣','2⃣','3⃣','4⃣'];

注意点として、複数リアクションをすることができるためこのプログラムではどれか一つにしか投票できないようにはできません。


まとめ

お疲れさまでした!とんでもなく長いnoteになってしまいましたが、一部でも読んでいただければ幸いです。

応用編の、投票コマンド以外のコードをまとめたものを、Githubに載せていますので是非ご利用ください。
https://github.com/ExtEOi/DiscordBotBase/tree/Advanced

質問や要望、こんなBotを作ってみたいから教えて欲しい!などあればコメントやDiscord (EOi#0777)にお声かけください!

この記事が気に入ったらサポートをしてみませんか?