N高等学校 一期生のnedewです。
若干の今更感はありますが、一時期の盛り上がり程ではないにしろマッチングに困らない程度には熱のあるOverwatchネタになります。
N高等学校アドベントカレンダー2018の6日目の記事です。
#前提
- Node.js環境でのdiscord.jsを用いたDiscord Bot開発を想定
- 本記事ではNode.js v10.4.1を使用
- discord.jsはJavaScript(Node.js)でDiscordのBot開発をするためのnpmパッケージ
- この記事ではdiscord.jsの詳細は省略するため、触ったことの無い方は先にドキュメント読むことを推奨
- discord.js Documents
#準備
##Botアカウントの準備とTokenの取得
1.DEVELOPER PORTAL に行く
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アカウントを追加
##新規ディレクトリの作成と諸々のインストール
必要なフォルダ、ファイルを作成します
$ mkdir overwatch-bot
$ cd overwatch-bot
$ touch app.js
今回使用するパッケージをインストールします。
Overwatchのプレイヤーデータ自体はここから見れるのでスクレイピングして持ってくるのも一つですが、その辺りを代わりにやってくれる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
のいずれか
'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
上記サンプルコードの様に、プレイヤーデータを取得するにはgetProfile()
、またはgetStats()
関数を使います。
##getProfile()
主にユーザネームやレベル、レート等のデータをJSONで取得できます。
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で取得できます。
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 (埋め込みメッセージ) を扱う
'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
##注意すること
Overwatchではちょっと前から自身のプロフィールの公開範囲を制限することが可能になりました。
そのためプロフィールを公開していないプレイヤーの情報を取得しようとすると、ユーザネームやレベル等の常時公開データ以外のデータを取得することができません。
指定したプレイヤーの公開/非公開状態を知るにはgetProfile()
またはgetStats()
で取得したJSONのprivate
プロパティで確認可能です(private: true
なら非公開、private: false
なら公開)
#Overwatch League関連情報を取得(おまけ)
Overwatch Leagueの情報も取得できるらしいので一応
全てJSON形式で取得されます
##owl.getLiveMatch()
記事執筆時にOWL配信がされていなかったので未確認ですが、関数名からして現在配信中のマッチの配信URL辺りを取得できるんだと思います
const overwatch = require('overwatch-api');
overwatch.owl.getLiveMatch((err, data) => {
// 任意の処理
});
##owl.getStandings()
Overwatch Leagueに参加しているそれぞれのチームデータを取得できます
const overwatch = require('overwatch-api');
overwatch.owl.getStandings((err, data) => {
// 任意の処理
});
##owl.getSchedule()
Overwatch Leagueのマッチスケジュールを取得できます
const overwatch = require('overwatch-api');
overwatch.owl.getSchedule((err, data) => {
// 任意の処理
});
#最後に
本記事で紹介しているプログラムは紹介用の簡略化されたものであり、見れば分かる通り想定しない入力があるとうまく動かないどころかエラー吐いて落ちるため、実際に運用する際は全ての入力に対応させて下さい。
OWから逃げるな