Overwatchのプレイヤーデータを返すDiscord Bot

N高等学校 一期生の[nedew](https://twitter.com/nedew)です。

若干の今更感はありますが、一時期の盛り上がり程ではないにしろマッチングに困らない程度には熱のあるOverwatchネタになります。


N高等学校アドベントカレンダー2018の6日目の記事です。
#前提
* Node.js環境での[discord.js](https://discord.js.org/#/)を用いたDiscord Bot開発を想定
    * 本記事ではNode.js v10.4.1を使用
* discord.jsはJavaScript(Node.js)でDiscordのBot開発をするためのnpmパッケージ
    * この記事ではdiscord.jsの詳細は省略するため、触ったことの無い方は先にドキュメント読むことを推奨
    * [discord.js Documents](https://discord.js.org/#/docs/main/stable/general/welcome)

#準備
##Botアカウントの準備とTokenの取得

1.[DEVELOPER PORTAL](https://discordapp.com/developers/applications/) に行く
2.「Create an application」をクリックし、アイコンや名前等任意のものに変更
3.左側にある「SETTING」メニューから「Bot」を選択、次の画面で「Add Bot」をクリックしBotを作成
4.「Click to Reveal Token」をクリックするとTokenが表示されるのでメモっておく
5.再度左側の「SETTING」メニューから「OAuth2」を選択
6.「SCOPES」欄で「bot」を選択
7.「BOT PERMISSIONS」欄で適切な権限を割り当てる（ここでは「Administrator」を付与）
8.手順6で「bot」を選択した際に「SCOPES」欄下部に表示されたURLにアクセスし、自身のサーバーへBotアカウントを追加

参考: [【Discord】Node.jsでBOTを作成する](https://blog.narumium.net/2018/08/05/%E3%80%90discord%E3%80%91node-js%E3%81%A7bot%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B/)

##新規ディレクトリの作成と諸々のインストール
必要なフォルダ、ファイルを作成します

```
$ mkdir overwatch-bot
$ cd overwatch-bot
$ touch app.js
```
今回使用するパッケージをインストールします。
Overwatchのプレイヤーデータ自体は[ここ](https://playoverwatch.com/ja-jp/career/pc/nedew-11506)から見れるのでスクレイピングして持ってくるのも一つですが、その辺りを代わりにやってくれる[**overwatch-api**](https://www.npmjs.com/package/overwatch-api)(非公式)というものが提供されてるので利用させてもらいましょう。

```
$ npm init
$ npm i discord.js --save
$ npm i overwatch-api --save
```

#とりあえず書いてみる
Botのいるチャンネルに`overwatch <battletag> <region> <platform>`という形での投稿がされた時に指定されたプレイヤーの名前、レベル、レートをBotに返信させるサンプルコードを書いてみます。

なお`<battletag>, <region>, <platform>`にはそれぞれ以下の値が入ります。

* `<battletag>`には`example#1234`といったBattleTag
* `<region>`にはゲームサーバ`us, eu, kr, cn, global`のいずれか
* `<platform>`には指定したプレイヤーがプレイしている媒体`pc, xbl, psn`のいずれか


```js:app.js
'use strict';

/*
Copyright (c) 2016-2018 Alfred Gutierrez
Released under the MIT license
https://github.com/alfg/overwatch-api/blob/master/LICENSE
*/
const overwatch = require('overwatch-api'); // overwatch-apiはMITライセンスで提供されています

const Discord = require('discord.js');
const client = new Discord.Client();
const token = 'ここに先程作成したBotアカウントのTokenを貼り付ける';

// ready
client.once('ready', () => {
  console.log('ready...');
});

// Botの参加しているチャンネル(DM含む)に発言があったとき
client.on('message', message => {

  // Bot自身の発言を無視
  if (message.author.bot) return;

  // メッセージをスペースで分割
  const args = message.content.split(/ +/);

  // メッセージの始まりが'overwatch'であれば
  if (args[0] === 'overwatch') {

    let tag = args[1].replace('#', '-'), // "#"を"-"に置き換える（example#1234 → example-1234）
        region = args[2],
        platform = args[3];
    
    // Profileデータを取得
    overwatch.getProfile(platform, region, tag, (err, data) => {

      if (err) {
        // Errorを出力
        console.error(err);
      } else {
        // 取得したデータからネーム、レベル、レートを抽出してBotに発言させる
        message.channel.send(
          `Name: **${data.username}**\n` +
          `Level: **${data.level}**\n` +
          `Rank: **${data.competitive.rank}**`
        );
      }

    });
  }
});

// Discordに接続
client.login(token);
```
実行して試してみましょう

```
$ node app
```

<img width="433" alt="スクリーンショット 2018-12-05 22.33.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/243714/f7a6ffd4-6f0d-9495-a27c-2dc0ddeba3c6.png">
ちゃんと取得できているようです。

上記サンプルコードの様に、プレイヤーデータを取得するには`getProfile()`、または`getStats()`関数を使います。
##getProfile()
主にユーザネームやレベル、レート等のデータをJSONで取得できます。

```js
const overwatch = require('overwatch-api');
 
const tag = 'nedew-11506',
      region = 'us',
      platform = 'pc';
 
overwatch.getProfile(platform, region, tag, (err, data) => {
  if (err) {
    console.error(err)
  } else {
    console.log(data);
});
```

```
{ username: 'nedew',
  level: 105,
  portrait:　'https://d15f34w2p8l1cc.cloudfront.net/overwatch/029b39829039b61f438d164fc9f1b5373fdd560f7b30fa13cbbf82aee6f2d4a9.png',
  endorsement:
   { sportsmanship: { value: 0.21, rate: 21 },
     shotcaller: { value: 0.21, rate: 21 },
     teammate: { value: 0.58, rate: 58 },
     level: 3,
     frame:　'https://d3hmvhl7ru3t12.cloudfront.net/svg/icons/endorsement-frames-aa182c1f63b51afa951daec63595791283ab97ea3a07f8d47abf9dc7aeda5cc67c786041042de0b8e427194ed084f7cee6b56fa984532199e7ea95bc12bbd995.svg#_3',
     icon:　//長すぎるので省略,
  private: false,
  games:
   { quickplay: { won: 147, played: undefined },
     competitive: { won: 42, lost: 35, draw: 3, played: 80, win_rate: 54.55 } },
  playtime: { quickplay: '37:11:55', competitive: '17:45:33' },
  competitive:
   { rank: 3340,
     rank_img:　'https://d1u1mce87gyfbn.cloudfront.net/game/rank-icons/rank-DiamondTier.png' },
  levelFrame:　'https://d15f34w2p8l1cc.cloudfront.net/overwatch/1055f5ae3a84b7bd8afa9fcbd2baaf9a412c63e8fe5411025b3264db12927771.png',
  star:　'https://d15f34w2p8l1cc.cloudfront.net/overwatch/8de2fe5d938256a5725abe4b3655ee5e9067b7a1f4d5ff637d974eb9c2e4a1ea.png' }
```
##getStats()
主に使用ヒーローや記録等のデータをJSONで取得できます。

```js
const overwatch = require('overwatch-api');
 
const tag = 'nedew-11506',
      region = 'us',
      platform = 'pc';
 
overwatch.getStats(platform, region, tag, (err, data) => {
  if (err) {
    console.error(err)
  } else {
    console.log(data);
});
```

```

{ username: 'nedew',
  level: 105,
  portrait: 'https://d15f34w2p8l1cc.cloudfront.net/overwatch/029b39829039b61f438d164fc9f1b5373fdd560f7b30fa13cbbf82aee6f2d4a9.png',
  endorsement:
   { sportsmanship: { value: 0.21, rate: 21 },
     shotcaller: { value: 0.21, rate: 21 },
     teammate: { value: 0.58, rate: 58 },
     level: 3,
     frame:　'https://d3hmvhl7ru3t12.cloudfront.net/svg/icons/endorsement-frames-aa182c1f63b51afa951daec63595791283ab97ea3a07f8d47abf9dc7aeda5cc67c786041042de0b8e427194ed084f7cee6b56fa984532199e7ea95bc12bbd995.svg#_3',
     icon:　'data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB3aWR0aD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxjaXJjbGUgcj0iMTUuOTE1NDk0MzA5MTg5NTQiIGZpbGw9IiMyYTJiMmUiIHN0cm9rZS1kYXNoYXJyYXk9IjIxIDc5IiBzdHJva2UtZGFzaG9mZnNldD0iMjUiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlPSIjZjE5NTEyIiBjeD0iNTAlIiBjeT0iNTAlIj48L2NpcmNsZT48Y2lyY2xlIHI9IjE1LjkxNTQ5NDMwOTE4OTU0IiBmaWxsPSJ0cmFuc3BhcmVudCIgc3Ryb2tlLWRhc2hhcnJheT0iNTggNDIiIHN0cm9rZS1kYXNob2Zmc2V0PSIxMDQiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlPSIjYzgxYWY1IiBjeD0iNTAlIiBjeT0iNTAlIj48L2NpcmNsZT48Y2lyY2xlIHI9IjE1LjkxNTQ5NDMwOTE4OTU0IiBmaWxsPSJ0cmFuc3BhcmVudCIgc3Ryb2tlLWRhc2hhcnJheT0iMjEgNzkiIHN0cm9rZS1kYXNob2Zmc2V0PSI0NiIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2U9IiM0MGNlNDQiIGN4PSI1MCUiIGN5PSI1MCUiPjwvY2lyY2xlPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBkeT0iLjNlbSIgZm9udC1mYW1pbHk9ImNlbnR1cnkgZ290aGljLGFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtd2VpZ2h0PSIzMDAiIGZvbnQtc2l6ZT0iMTYiIHN0cm9rZT0iI2Y2ZjZmNiIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSIjZjZmNmY2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj4zPC90ZXh0Pjwvc3ZnPg==' },
  private: false,
  stats:
   { top_heroes: { quickplay: [Object], competitive: [Object] },
     combat: { quickplay: [Array], competitive: [Array] },
     match_awards: { quickplay: [Array], competitive: [Array] },
     assists: { quickplay: [Array], competitive: [Array] },
     average: { quickplay: [Array], competitive: [Array] },
     miscellaneous: { quickplay: [Array], competitive: [Array] },
     best: { quickplay: [Array], competitive: [Array] },
     game: { quickplay: [Array], competitive: [Array] } } }

```
##使用例
試しに`getProfile()`、`getStats()`で取得したプレイヤーデータを利用し、整形したembedメッセージをBotに発言させるようにしてみます。

embed（埋め込み）メッセージについては別の記事で解説しているので興味ある方は覗いてみてください。
[Discord.jsでembed (埋め込みメッセージ) を扱う](https://qiita.com/nedew/items/4e0c20c1a89e983a6992)



```js:app.js
'use strict';

/*
Copyright (c) 2016-2018 Alfred Gutierrez
Released under the MIT license
https://github.com/alfg/overwatch-api/blob/master/LICENSE
*/
const overwatch = require('overwatch-api'); // overwatch-apiはMITライセンスで提供されています

const Discord = require('discord.js');
const client = new Discord.Client();
const token = 'ここに先程作成したBotアカウントのTokenを貼り付ける'

// ready
client.once('ready', () => {
  console.log('ready...');
});

// Botの参加しているチャンネル(DM含む)に発言があったとき
client.on('message', message => {

  // Bot自身の発言を無視
  if (message.author.bot) return;

  // メッセージをスペースで分割
  const args = message.content.split(/ +/);

  // メッセージの始まりが'overwatch'であれば
  if (args[0] === 'overwatch') {

    // "overwatch example#1234 us pc"
    let tag = args[1].replace('#', '-'),
        region = args[2],
        platform = args[3];
    
    // Get Profile Data
    const owGetProf = new Promise(function(resolve, reject) {
      overwatch.getProfile(platform, region, tag, (err, data) => {
        err ? reject() : resolve(data);
      });
    });
    
    // Get Stats Data
    const owGetStats = new Promise(function(resolve, reject) {
      overwatch.getStats(platform, region, tag, (err, data) => {
        err ? reject() : resolve(data);
      });
    });

    Promise.all([owGetProf, owGetStats])
    .then((result) => {

      const gottenProfile = result[0], gottenStats = result[1];
      // Battle Tag
      const playerName = tag.replace('-', '#');
    
      message.channel.send({embed: {
        color: 16751616,
        author: {
          name: 'OVERWATCH',
          icon_url: gottenProfile.portrait
        },
        title: 'Stats for ' + playerName,
        url: `https://playoverwatch.com/ja-jp/career/${platform}/${playerName.replace('#', '-')}`,
        timestamp: new Date(),
        thumbnail: {
          url: gottenProfile.portrait
        },
        image: {
          url: gottenProfile.competitive.rank_img
        },
        footer: {
          icon_url: message.client.user.avatarURL,
          text: '© Overwatch Bot'
        },
        fields: [
          {
            name: 'Account Stats',
            value: `Level: **${gottenProfile.level}**\n` +
                   `Rank: **${gottenProfile.competitive.rank}**\n` +
                   `Endorsement Level: **${gottenProfile.endorsement.level}**`,
            inline: true
          },
          {
            name: 'Medals in Quick Play',
            value: `Golden Medals: **${gottenStats.stats.match_awards.quickplay[2].value}**\n` +
                   `Silver Medals: **${gottenStats.stats.match_awards.quickplay[3].value}**\n` +
                   `Bronze Medals: **${gottenStats.stats.match_awards.quickplay[4].value}**`,
            inline: true
          },
          {
            name: 'Match Record',
            value: `__[QUICK PLAY]__\n` +
                   `Won: **${gottenStats.stats.game.quickplay[0].value}**\n` +
                   `Playtime: **${gottenStats.stats.game.quickplay[1].value.split(':')[0]}h**\n` +
                   `__[COMPETITIVE]__\n` +
                   `Won: **${gottenProfile.games.competitive.won}**\n` +
                   `Lost: **${gottenProfile.games.competitive.lost}**\n` +
                   `Draw: **${gottenProfile.games.competitive.draw}**\n` +
                   `Win Rate: **${Math.round(gottenProfile.games.competitive.win_rate)}%**`,
            inline: true
          },
          {
            name: 'Most Played Heroes',
            value: `__[QUICK PLAY]__\n` +
                   `1. **${gottenStats.stats.top_heroes.quickplay.played[0].hero}** - ${gottenStats.stats.top_heroes.quickplay.played[0].played}\n` +
                   `2. **${gottenStats.stats.top_heroes.quickplay.played[1].hero}** - ${gottenStats.stats.top_heroes.quickplay.played[1].played}\n` +
                   `3. **${gottenStats.stats.top_heroes.quickplay.played[2].hero}** - ${gottenStats.stats.top_heroes.quickplay.played[2].played}\n` +
                   `__[COMPETITIVE]__\n` +
                   `1. **${gottenStats.stats.top_heroes.competitive.played[0].hero}** - ${gottenStats.stats.top_heroes.competitive.played[0].played}\n` +
                   `2. **${gottenStats.stats.top_heroes.competitive.played[1].hero}** - ${gottenStats.stats.top_heroes.competitive.played[1].played}\n` +
                   `3. **${gottenStats.stats.top_heroes.competitive.played[2].hero}** - ${gottenStats.stats.top_heroes.competitive.played[2].played}`,
            inline: true
          }
        ]
      }});
    }).catch(() => {
      return message.reply('エラーが発生しました');
    });
  }
});

// Discordに接続
client.login(token);
```
`app.js`を実行し、Botのいるチャンネルでコマンドを入力

```
$ node app
```
<img width="887" alt="スクリーンショット 2018-12-06 1.59.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/243714/f9dce217-2b43-503e-b2e2-8ad66d2c0b51.png">



##注意すること
Overwatchではちょっと前から自身のプロフィールの公開範囲を制限することが可能になりました。
そのためプロフィールを公開していないプレイヤーの情報を取得しようとすると、ユーザネームやレベル等の常時公開データ以外のデータを取得することができません。

指定したプレイヤーの公開/非公開状態を知るには`getProfile()`または`getStats()`で取得したJSONの`private`プロパティで確認可能です（`private: true`なら非公開、`private: false`なら公開）

#Overwatch League関連情報を取得（おまけ）
Overwatch Leagueの情報も取得できるらしいので一応
全てJSON形式で取得されます

##owl.getLiveMatch()
記事執筆時にOWL配信がされていなかったので未確認ですが、関数名からして現在配信中のマッチの配信URL辺りを取得できるんだと思います

```js
const overwatch = require('overwatch-api');

overwatch.owl.getLiveMatch((err, data) => {
  // 任意の処理
});
```


##owl.getStandings()
Overwatch Leagueに参加しているそれぞれのチームデータを取得できます

```js
const overwatch = require('overwatch-api');

overwatch.owl.getStandings((err, data) => {
  // 任意の処理
});
```


##owl.getSchedule()
Overwatch Leagueのマッチスケジュールを取得できます

```js
const overwatch = require('overwatch-api');

overwatch.owl.getSchedule((err, data) => {
  // 任意の処理
});
```

#最後に
本記事で紹介しているプログラムは紹介用の簡略化されたものであり、見れば分かる通り想定しない入力があるとうまく動かないどころかエラー吐いて落ちるため、実際に運用する際は全ての入力に対応させて下さい。


**OWから逃げるな**