TOP

このエントリーをはてなブックマークに追加

Node.js × Twitch


はじめに

このページは、 Node.js × Twitch, Twitter, OBS の続きです。
Twitch イベントを Webhook や tmi.js というライブラリを使用して Node.js で受け取ったり、チャットを投げたりする部分について記載します。以下画像の赤枠部分です。
play_with_nodejs_twitch

赤枠以外の部分については、以下リンク先に別ページでまとめています。GitLab や Linux, Docker, nginx については記載していません。



トークン発行までの流れ

TwitchからWebhook経由でイベントの通知を受信したりAPIを発行したりするには、認証サーバが発行したトークンを使用する必要があります。
TwitchはOAuth 2.0に準拠した認証フローを採用しているため、トークンの発行手順もOAuth 2.0に従います。
Twitchのトークン発行手順を追いながらOAuth 2.0について理解するページを別にまとめているので、手順についてはこのリンク先を参照してください。



Twitch のユーザー

Twitch ではユーザーを示す項目は大きく3種類あります。
API や Webhook を使う際に理解しておく必要があるため、ここで整理しておきます。

  • user_name/login_name:ログインするときのid。アカウントの設定画面では「ユーザー名」と訳されている。後から変更できる。
  • display_name:チャット欄などに表示される名前。アカウントの設定画面では「表示名」と訳されている。後から変更できる。設定していないユーザーもいる。
  • user_id:Twitch が裏で採番している一意なid。通常意識しない。変更不可。

呼び方は統一されていない気がしますが、項目としてはこの3つがあります。
display_name が設定されているユーザーの場合、チャットしたときの名前が「あああ(aaa)」と表示されます。この「あああ」が display_name で「aaa」が user_name です。
Twitch の API や Webhook を使用するときは user_name と user_id を結構使うので、この違いは把握しておきたい。
下記リンク先から user_name と user_id を相互に変換することができるのでブックマークしておくと良いでしょう。
Convert Twitch Username to Channel ID



Node.js × tmi.js

tmi.js というライブラリを使うと、視聴者のチャットなど、様々なイベントに反応する処理を実装できます。 Node.js の EventEmitter のような仕組みでチャットをトリガーに処理を動かすことができます。
また、チャットを投げることもできます。
tmi.js についてはこちらも参照 github tmi.js
EventEmitter については非同期のイベント駆動型ランタイムを参照してください。

tmi.js はチャット以外にも様々なイベントを拾って処理を開始することができます。ここではそのすべてについては説明しません。イベントの全量は以下リンク先を参照して下さい。
github tmijs/docs

イベントの中には Webhook で拾えるものと被っているものが結構あります。
Webhook よりは tmi.js を使った方が(たぶん)早いので基本的に tmi.js を使えばいいと思いますが、raid は Webhook の方が使い勝手が良いと思います。
tmi.js の raid だと、 display_name を設定しているユーザーの場合、display_name のみが返され user_name を取得できないからです。display_name をキーに user_name を返してくれるAPI は(たぶん)ないので、結構困ります。
Webhook であれば user_name と display_name の両方を取得できます。
tmi.js を改造して両方取得するやり方を紹介しているブログ記事もありますが、外部ライブラリを改造するとバージョンアップの度に書き直す必要があったり、改造が原因で予期せぬ挙動をすることも考えられるのであまりお勧めできません。一応当該記事のリンクを張っておきます。
Zenn tmi.jsでRaidのユーザ名・表示名を別々に取得したい

以下は、誰かがチャットするたびに稼働する処理の例です。Message イベントをトリガーにしています。
事前に以下コマンドで tmi.js をインストールしている前提です。
npm i tmi.js

twitch_bot.js
'use strict';
import tmi from 'tmi.js';

const opts = {
  identity: {
    username: 'user_name',
    password: 'oauth:'+[access_token],
  },
  channels: [
    [channel_name]
  ]
};
const twClientBot = new tmi.client(opts)

// チャットに反応
twClientBot.on('message', (channel, userstate, msg, self) => {
  if (self) return; // Ignore messages from the bot
  // 実装したい処理を書く

  // チャットを投げる
  twClientBot.say(channel, [message]);
});

// Connect to Twitch:
twClientBot.connect();

"[ ]" 内には以下の値を設定します。

  • access_token:Tokenの発行で取得した User access token
  • channel_name:対象チャンネルのチャンネル名(=user_name)
  • message:投稿するメッセージ

twClientBot.on('message', (channel, userstate, msg, self) => {
  ...中略...
});

の部分はチャットが入力される度に稼働する処理です。channel にはチャンネル名、 userstate にはチャットしてくれたユーザーの情報、 msg にはチャットに入力された文言、 self は自分自身かどうかを示す bool 型の値が入っています。
第2引数の userstate には以下のようなユーザー情報が json 形式で入っているので、主にこの userstate と msg(ユーザーがチャットに投げた文言) を使って自由に処理を組み立てることができます。

{
  "badge-info": {
    "subscriber": "8"
  },
  "badges": {
    "broadcaster": "1",
    "subscriber": "0",
    "premium": "1"
  },
  "client-nonce": "sample",
  "color": "#0000FF",
  "display-name": "sample",
  "emotes": null,
  "first-msg": false,
  "flags": null,
  "id": "sample",
  "mod": false,
  "returning-chatter": false,
  "room-id": "sample",
  "subscriber": true,
  "tmi-sent-ts": "sample",
  "turbo": false,
  "user-id": "sample",
  "user-type": null,
  "emotes-raw": null,
  "badge-info-raw": "subscriber/8",
  "badges-raw": "broadcaster/1,subscriber/0,premium/1",
  "username": "sample",
  "message-type": "chat"
}


以上で tmi.js については終わりです。



Node.js × Webhook

Webhook でイベントの通知を受け取るためには、まず受け取るサーバを構築する必要があります。以下リンク先で詳細に解説されていますが、ここでも記載します。
Getting Events Using Webhook Callbacks

Webhook でイベントを受け取るためには、まず以下のような形式で curl コマンドを実行する必要があります。

curl -X POST -d '{
  "type":"[subscription_type]",
  "version":"1",
  "condition":{
    "broadcaster_user_id":"[user_id]"
  },
  "transport":{
  "method":"webhook",
  "callback":"[callback_url]",
  "secret":"[secret]"
  }
}'
-H "Content-Type: application/json"
-H "Authorization: Bearer [access_token]"
-H "client-id: [client_id]"
https://api.twitch.tv/helix/eventsub/subscriptions

  • subscription_type:Subscription Types の Name 列の値を設定。
    例として、配信開始イベントを受け取るなら 'stream.online' を、チャンネルポイントが使用されたイベントを受け取るなら 'channel.channel_points_custom_reward_redemption.add' を指定。
  • user_id:イベントを受け取りたいユーザーの user_id(基本的には自分の user_id を指定するが、他人の user_id を指定すれば、他人の配信開始などのイベントを受信することも可能)
  • callback_url:Webhook でイベントの通知を受け取りたいURL
    (https://[ホスト名]/[パス])
    ※指定するURLは443番ポートでSSL通信を使用している必要があります。
  • secret:Webhook を受け取ったサーバで認証に使用する値
  • access_token:Tokenの発行で取得した App access token
  • client_id:アプリケーションの登録で取得したクライアントID

上記 curl を実行すると callback_url に確認のリクエストが飛んでくる。
リクエストヘッダーの 'Twitch-Eventsub-Message-Type' に 'webhook_callback_verification' という値が入ってくるので、その場合は body 部の challenge を status 200 でそのまま返せばOK。
確認のリクエストを受け取るためのサーバ側の Node.js のコードは以下の通り。http サーバを構築してリクエストを受け取れるようにしています。
全量のコードを後に載せていますが、長いのでここでは抜粋)
事前に以下コマンドで express と body-parser というライブラリをインストールしている前提です。
npm i express
npm i body-parser

確認のリクエストを受ける部分抜粋
'use strict';
import {createServer} from 'http';
import express from 'express';
import bodyParser from 'body-parser';

const app = express();
app.use(bodyParser.json())

app.post([パス], async (req, res) => {
  const MESSAGE_TYPE = 'Twitch-Eventsub-Message-Type'.toLowerCase();
  const messageType = req.header(MESSAGE_TYPE).toLowerCase();
  const MESSAGE_TYPE_VERIFICATION = 'webhook_callback_verification';
  // 初回認証
  if (messageType === MESSAGE_TYPE_VERIFICATION) {
    res.status(200).send(req.body.challenge);
  } else {
    ...
  }
}

// 中略
const server = createServer(app);
server.listen(8080);

以下の curl コマンドを実行すると、問題なく処理されているかを確認することができます。access_token, client_id に設定する値は先の curl コマンドの時と同じです。

curl -X GET 'https://api.twitch.tv/helix/eventsub/subscriptions' \
-H 'Authorization: Bearer [access_token]' \
-H 'Client-Id: [client_id]'

以下のようなレスポンスが返ってきます。 status が enabled になっていれば問題なしです。

{
  "total":1,
  "data":[{
    "id":"xxxxxxxxxx",
    "status":"enabled",
    "type":[指定した subscription_type],
    "version":"1",
    "condition":{
      "broadcaster_user_id":[指定した user_id]
    },
    "created_at":"yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSSSZ",
    "transport":{
      "method":"webhook",
      "callback":[指定した callback_url]
    },
    "cost":0
  }],
  "max_total_cost":10000,"total_cost":0,"pagination":{}
}


以下が(ほぼ)全量の Node.js のコードです。Webhook でイベントの通知を受け取った時の処理も記載しています。
Twitch から送られてきたものであることを確認するため、HMAC認証のコードを入れています。これについては Verifying the event message も参照してください。

(ほぼ)全量
'use strict';
import crypto from 'crypto';
import {createServer} from 'http';
import express from 'express';
import bodyParser from 'body-parser';

const app = express();
app.use(bodyParser.json())

// Notification request header
const TWITCH_MESSAGE_ID = 'Twitch-Eventsub-Message-Id'.toLowerCase();
const TWITCH_MESSAGE_TIMESTAMP = 'Twitch-Eventsub-Message-Timestamp'.toLowerCase();
const TWITCH_MESSAGE_SIGNATURE = 'Twitch-Eventsub-Message-Signature'.toLowerCase();
const MESSAGE_TYPE = 'Twitch-Eventsub-Message-Type'.toLowerCase();

// Notification message types
const MESSAGE_TYPE_VERIFICATION = 'webhook_callback_verification';
const MESSAGE_TYPE_NOTIFICATION = 'notification';

// event type
const SUBSCRITION_TYPE = 'Twitch-Eventsub-Subscription-Type'.toLowerCase();

// Prepend this string to the HMAC that's created from the message
const HMAC_PREFIX = 'sha256=';

// post でリクエスト時に処理するコールバック関数指定
app.post([パス], async (req, res) => {
  let secret = getSecret();
  let hmacMsg = getHmacMessage(req);
  let hmac = HMAC_PREFIX + getHmac(secret, hmacMsg);  // Signature to compare

  // hmac認証失敗なら速攻で返す
  if (!verifyMessage(hmac, req.headers[TWITCH_MESSAGE_SIGNATURE])) {
    res.sendStatus(403);
    return
  }

  const messageType = req.header(MESSAGE_TYPE).toLowerCase();
  // 初回認証
  if (messageType === MESSAGE_TYPE_VERIFICATION) {
    res.status(200).send(req.body.challenge);
  // イベント通知
  } else if (messageType === MESSAGE_TYPE_NOTIFICATION) {
    const subscType = req.header(SUBSCRITION_TYPE).toLowerCase();
    switch (subscType) {
      // 配信開始時の処理
      case 'stream.online':
        // 何か処理
        res.sendStatus(200);
        break;
      // チャンネルポイントが使用された時の処理
      case 'channel.channel_points_custom_reward_redemption.add':
        // 何か処理
        res.sendStatus(200);
        break;
      // raid を受けた/した
      case 'channel.raid':
        // 何か処理
        res.sendStatus(200);
        break;
      // 配信終了
      case 'stream.offline':
        // 何か処理
        res.sendStatus(200);
        break;
      default:
        console.log(new Date() + ':unexpected subscription type.');
        res.sendStatus(200);
    }
  }
});

const server = createServer(app);
server.listen(8080);

function getSecret() {
  // TODO: Get your secret from secure storage. This is the secret you passed 
  // when you subscribed to the event.
  return '<your secret goes here>';
}

// Build the message used to get the HMAC.
function getHmacMessage(req) {
  return (req.headers[TWITCH_MESSAGE_ID] + 
    req.headers[TWITCH_MESSAGE_TIMESTAMP] + 
    JSON.stringify(req.body));
}

// Get the HMAC.
function getHmac(secret, hmacMsg) {
  return crypto.createHmac('sha256', secret)
    .update(hmacMsg)
    .digest('hex');
}

// Verify whether our hash matches the hash that Twitch passed in the header.
function verifyMessage(hmac, verifySignature) {
  return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(verifySignature));
}


以上で Webhook については終わりです。



Node.js × Twitch API

Twitch には様々な API が用意されており、それを呼び出すことで様々な処理を行うことができます。
API の一覧と仕様は Twitch API Reference 参照。リクエストパラメータやリクエストボディ、レスポンスの内容、必要なスコープがわかるようになっている。
今回は例として、ユーザーを Ban する Node.js の関数を作ってみます。
user_name を引数で受け取り、Get Users API で user_name を元に user_id を取得し、それを使用して Ban User API で ban するという流れです。
※access_token やクライアントID などの機密情報は、外部ファイルに json 形式などで保存した上で、そこから取得するようにしてください。

async function banUser(banUserName) {
  // user_name から user_id を取得
  const get_users_url = `https://api.twitch.tv/helix/users?login=${banUserName}`
  let result = await fetch(get_users_url , {
    headers: {
      "Authorization": "Bearer " + [access_token],
      "Client-Id": [clientID]
    },
  })

  let banUserInfo = await result.json()
  let banUserId = banUserInfo.data[0].id

  // 対象ユーザーをban
  const ban_api_url = `https://api.twitch.tv/helix/moderation/bans?broadcaster_id=[配信者のuser_id]&moderator_id=[モデレータのuser_id]`
  return await fetch(ban_api_url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + [access_token],
      'Client-Id': [clientID]
    },
    body: JSON.stringify({
      'data': {
        'user_id': banUserId
      },
    }),
  })
}

"[ ]" には以下の値を設定します。


以上で Twitch の API については終わりです。



その他の実装

Node.js と Twitch の組み合わせについては以上です。その他の組み合わせについては以下リンク先を参照してください。




参考サイト