Google Apps ScriptでDiscordにbotを作る

Slackと同じElectronで出来ているDiscordはSlackのbotの作り方と親和性が高く、ほとんど同じようにDiscord用のbotを作ることが出来ます。
今回はFPSゲームであるApex Legendsのデータトラッカーを利用してDiscordにbotを作成してみます。

概要

ある時間になるとGASがApex LegendsのプレイヤーデータをTRNから取得し、Discordに投稿する

  1. トリガーが実行される。
  2. TRNからApex Legendsのプレイヤーデータを取得する。
  3. プレイヤーデータをDiscordにPOSTする。
  4. Discordのチャンネルに投稿される。

完成イメージはこんな感じです。
embedという形式で投稿しています。

また、embedを使わない普通の投稿も作ってみました。

作成

TRNでのApex trackerの使い方

現在Apex Legendsは公式のデータトラック用APIを公開していませんが、TRNというデータトラック用のサイトを経由してプレイヤー情報を取得することが出来ます。
TRN Apex Legends API

Apex TrackerをAPIとして使用するためにはAPIのキーが必要です。
TRNのアカウントを登録してログイン後、上のURLの
「Manage or Create API Keys」をクリックします。

適当に使いたい理由を書いてメールを送信すると使えるようになります。
その後APIキーを見ることが出来ます。
管理画面に遷移してもキーを確認できます。

APIキーを取得できたので、トラック情報が取得できるかcurlで確認してみます。
コマンドプロンプトから以下のコマンドを打ちます。
※windowsではcurlコマンドを使えるように別途インストールが必要です。

2021/4/11追記:こちらTRNのAPIバージョンが変わりURLが変わっておりました。

curl https://public-api.tracker.gg/apex/v1/standard/profile/5/[プレイヤー名] -H “TRN-API-KEY:[取得したAPIキー]”
curl https://public-api.tracker.gg/v2/apex/standard/profile/[platform]/[プレイヤー名] -H "TRN-API-KEY:[取得したAPIキー]"

[platform]…プレイしているプラットフォームを入れる。現在origin, play station Network, xbox Liveに対応している。

  • Origin … “origin”
  • PlayStationNetwork … “psn”
  • Xbox Live … “xbl”

プレイヤーデータが返ってきたら無事データを取得できています。

{
  "data": {
    "id": "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX",
    "type": "player",
    "children": [
      {
        "id": "legend_5",
        "type": "",
        "metadata": {
          "legend_name": "Bloodhound",
          "icon": "https://media.contentapi.ea.com/content/dam/apex-legends/images/2019/01/legends-character-tiles/apex-grid-tile-legends-bloodhound.png.adapt.crop16x9.png",
          "bgimage": "https://trackercdn.com/cdn/apex.tracker.gg/legends/bloodhound-concept-bg-small.jpg"
        },
        "stats": [
          {
            "metadata": {
              "key": "SeasonWins",
              "name": "Season Wins",
              "categoryKey": "game",
              "categoryName": "Game",
              "isReversed": false
            },
            "value": 11.0,
            "percentile": 2.1,
            "rank": 3688,
            "displayValue": "11",
            "displayRank": "3,688"
          },
          {
            "metadata": {
              "key": "SeasonDamage",
              "name": "Season Damage",
              "categoryKey": "game",
              "categoryName": "Game",
              "isReversed": false
            },
            "value": 27452.0,
            "percentile": 3.0,
            "displayValue": "27,452",
            "displayRank": ""
          },
          {
            "metadata": {
              "key": "SeasonKills",
              "name": "Season Kills",
              "categoryKey": "game",
              "categoryName": "Game",
              "isReversed": false
            },
            "value": 80.0,
            "percentile": 3.8,
            "rank": 9927,
            "displayValue": "80",
            "displayRank": "9,927"
          }
        ]
      },

...
...
}

あとはこのコマンドをGASから打ち込むだけです。

2021/4/11追記:APIのバージョンが上がり、取得できる情報もAPIも増えたようです。

https://tracker.gg/developers/docs/titles/apex

ちなみに、プレイヤーデータはApex Legends内で自分が装備しているバナーの情報しか取得できないです。 バナーを変えると取得できるデータも変わります。

情報が持ってこれない場合、TRNアカウントとApex Legendsアカウントが紐づいていないのでTRN上でアカウントを認識させましょう。

キャラクターはTRNのダッシュボード上の「ADD LEGEND」で追加できます。
Apex Legendsを実際に起動した状態で、ゲーム内でトラッカーに追加したいキャラが選択された状態で「FORCE UPDATE」を押すと反映されます。

トラッカー情報取得まわり

ApexTrack.gs

var key = "[取得したAPIキー]";

function getApexTrack(member) {
  var trackUrl = member.trackUrl;
  
  var params={
    headers:{
      "TRN-API-KEY":key
    },
    method:"get"
  };
  
  var res = UrlFetchApp.fetch(trackUrl,params);
  
  //Logger.log(res);
  return JSON.parse(res);
}

後述しますが、引数のmemberオブジェクトにはプレイヤーのtrackURLが含まれていて、curlを叩く先のURLとなっています。

Discordまわり

Incoming Webhooksの作成 ※要管理者権限

チャンネルの設定画面の下段に「Incoming Webhooks」という項目があり、そこからwebhookを作成します。

チャンネルの横の設定をクリックし、

Webhooks項目の「Webhookを作成」ボタンをクリック

Webhookの設定画面で、
・Bot名
・投稿するチャンネル名
・アイコン画像
を設定します。
下段のWebhookURLがGASで使用するURLになります。

通常のテキストによる投稿

postDiscord.gs

function testPost() {
  var text = "text";
  postDiscord(text);
}

function postDiscord(text,url) {
  
  //DiscordのIncoming Webhookで取得したURL
  var url="https://discordapp.com/api/webhooks/9999999999999999/XXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
  //URLの一番右の文字列
  var token="XXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
  var channel="#general";
  var username="ApexTrackerBOT";
  var parse = "full";
  var method= "post";
  
  
  var jsonData={
    "token":token,
    "channel":channel,
    "username":username,
    "content":text,
    "parse":parse,
  }
  
  var options={
    "method":method,
    "headers":{"Content-type":"application/json"},
    "payload":JSON.stringify(jsonData),
    "muteHttpExceptions":true
  };
  
  UrlFetchApp.fetch(url,options);
}

testPost()を実行すると、チャンネルにテスト投稿されます。

Embedを利用した投稿

Embedとは、名前と実際の値というペアを良い感じに出してくれる機能です。
Discordだけでなく、Slackにも同じ機能があります。

postDiscordEmbed.gs

function testPostEmbed() {
  
  var embed = {};
  
  embed.fields = [];
  embed.fields.push({
    name: 'Test',
    value: '-------------------'
  });
  
  postDiscordEmbed("",embed);
}

function postDiscordEmbed(msg,embed) {
  
  //DiscordのIncoming Webhookで取得したURL
  var url="https://discordapp.com/api/webhooks/9999999999999999/XXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
  //URLの一番右の文字列
  var token="XXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
  var channel="#bot";
  var username="ApexTrackerBOT";
  var parse = "full";
  var method= "post";
  
  var jsonData={
    "token":token,
    "channel":channel,
    "username":username,
    "content":msg,
    "embeds":, 
    "parse":parse,
  }
  
  var options={
    "method":method,
    "headers":{
      "Content-type":"application/x-www-form-urlencoded",
      "User-Agent":"DiscordBot"
    },
    "payload":JSON.stringify(jsonData),
    "muteHttpExceptions":true
  };
  UrlFetchApp.fetch(url,options);
}

testPostEmbed()を実行すると、チャンネルにテスト投稿されます。

別のチャンネル用に新たに作成したBotで投稿したため、画像がまだ設定されていませんでした。

一つのAppとしてまとめる

テキスト形式で投稿する(非Embed)

main.gs

var members = {
  player1: {
    name : 'player1',
    imgUrl : '[メンバーのアイコンとして使いたい画像URL]',
    trackUrl : " https://public-api.tracker.gg/v2/apex/standard/profile/origin/[プレイヤー名]"
  },
  player2: {
    name : 'player2',
    imgUrl : '[メンバーのアイコンとして使いたい画像URL]',
    trackUrl : " https://public-api.tracker.gg/v2/apex/standard/profile/origin/[プレイヤー名]"
  }
};

function memberData_text() {
  for(var key in members) {
    
    var legends = getCharactorsData(getApexTrack(members[key]));
    
    for (var i=0; i<legends.length; i++) {
      var text = makeText(members[key],legends[i]);
      
      postDiscord(text);
      //キャラ分連続投稿するため、0.5秒遅延させて投稿が反映されるようにしている
      Utilities.sleep(500);
    }
  }
}

function makeText(member,legend) {
  
  var text = []
  
  text.push("**【Apex Legends DATA】" + member.name + "**");
  text.push("**Legend Name:" + legend['legendName'] + "**");
  delete legend['legendName'];
  
  text.push(legend['bgimage']);
  delete legend['bgimage'];
  
  text.push("==========================================");
  
  for(var key in legend) {
    text.push(key + " : " + legend[key]);
  }
  
  text.push("==========================================");
  
  return text.join("\n");
}

function getCharactorsData(jsonData) {
  
  var charactors = [];
  
  for each(var charactor in jsonData['data']['children']) {
    
    //初期化とともにレジェンド名を設定
    var legend = {legendName: charactor['metadata']['legend_name']};
    //アイコンを設定
    legend['bgimage'] = charactor['metadata']['bgimage'].toString();
    
    //ゲーム上のバナーで設定している項目を設定
    for(var i=0; i<charactor['stats'].length; i++) { 
      legend[charactor['stats'][i]['metadata']['key'].toString()] = charactor['stats'][i]['value'].toString();
    } 
    charactors.push(legend);
  }
  //Logger.log(charactors);
  return charactors;
}

流れを説明します。

  1. トリガーからメインの関数が実行される(memberData_text())
  2. membersに格納されたプレイヤー分のキャラクターデータリストを取得する。
    (getCharactorsData(jsonData))
  3. 取得できたキャラクターデータ分、投稿内容を組み立ててDiscordに投稿する。(makeText(member,legend))

Embed形式で投稿する

var members = {
  player1: {
    name : 'player1',
    imgUrl : '[メンバーのアイコンとして使いたい画像URL]',
    trackUrl : " https://public-api.tracker.gg/v2/apex/standard/profile/origin/[プレイヤー名]"
  },
  player2: {
    name : 'player2',
    imgUrl : '[メンバーのアイコンとして使いたい画像URL]',
    trackUrl : " https://public-api.tracker.gg/v2/apex/standard/profile/origin/[プレイヤー名]"
  }
};

function memberData_embed() {
  for(var key in members) {
    
    var legends = getCharactorsData(getApexTrack(members[key]));
    
    var embed = makeEmbed(members[key], legends);
    
    postDiscordEmbed("",embed);
  }
}

function makeEmbed(member, legend) {
  
  var embed = {};
  
  embed.title = "Apex TRN";
  embed.description = "Apex Legends Tracker";
  embed.url = "https://thetrackernetwork.com/";
  embed.color = 3329330;
  embed.author = {name: member.name, icon_url: member.imgUrl};
  embed.fields = [];
  
  for(var i=0; i<legend.length; i++) {
    
    embed.fields.push({
      name: "__" + legend[i]['legendName'] + "__",
      value: "==============================="
    });
    
    delete legend[i]['legendName'];
    delete legend[i]['bgimage'];
    
    for(var key in legend[i]) {
      embed.fields.push({
        name: key,
        value: legend[i][key],
        inline: true
      });
    }
  }
  return embed;
}

function getCharactorsData(jsonData) {
  
  var charactors = [];
  
  for each(var charactor in jsonData['data']['children']) {
    
    //初期化とともにレジェンド名を設定
    var legend = {legendName: charactor['metadata']['legend_name']};
    //アイコンを設定
    legend['bgimage'] = charactor['metadata']['bgimage'].toString();
    
    //ゲーム上のバナーで設定している項目を設定
    for(var i=0; i<charactor['stats'].length; i++) { 
      legend[charactor['stats'][i]['metadata']['key'].toString()] = charactor['stats'][i]['value'].toString();
    } 
    charactors.push(legend);
  }
  //Logger.log(charactors);
  return charactors;
}

流れはテキスト形式の時と同じ流れですが、Embed形式の場合はキャラごとに投稿する必要はなく、まとめてEmbed用の配列に格納すればOKです。
ちなみに、画像URLも格納できますがイメージはDiscordに表示されません。

テストしていて気付いたのですが、画像URLを投稿した時の動作がSlackとDiscordで違っていて、
・Slackの場合はURLのすぐ下に画像が差し込まれて表示される
・Discordの場合は投稿文の下にまとめて画像が表示される
でした。

トリガー

毎日Botがチャンネルに投稿するようにしました。

botカテゴリの最新記事