where

FaceRigに自作3Dモデルを入れる事や、その他技術っぽいことについてメモするだけのブログです

Twitch配信開始の通知を自動でツイートする

はじめに

旧記事から半分ほど作り直した Twitch 配信通知システムの作り方メモです。


やりたいこと

準備

Twitch Developer 登録

Twitch Developer

控えておくもの

  • クライアントID
  • クライアントシークレット

Twitter Developer 登録

Twitter Developer

登録について
英語でアプリの目的とかを書いて申請する必要あり。
自分の場合は、英語で申請した後に日本語のメールが来て、英語と同じような内容を日本語でちゃんと書いて返信したら登録できた。
単純な通知アプリを作りたい、というのの他に、個人で使う用です、とか、勉強がてらです、とか書いたけど必要だったかは分からない。

プロジェクトを登録出来たら Settings の App permissions で Write を含むように変更する。

控えておくもの

Twitchから情報取得

アクセストークン取得
Authentication | Twitch Developers

App access tokens を取得したいので、
Client credentials flow を使う。

curl -X POST -H "Content-Type: application/json" "https://id.twitch.tv/oauth2/token?client_id=クライアントID&client_secret=クライアントシークレット&grant_type=client_credentials"

返ってきた access_token を控える。

ユーザID取得

curl "https://api.twitch.tv/helix/users?login=ユーザー名" -H "Content-Type: application/json" -H "Authorization: Bearer Twitchアクセストークン" -H "client-id: クライアントID"

控えておくもの

  • Twitchアクセストーク
  • ユーザーID

Firebase Functions 登録

Firebase

Functions を使うために料金プランをBlazeにする。
料金参考:Firebaseで無料枠の範囲内のはずが課金されたのならこれが理由かも? - Qiita

控えておくもの

  • コールバックURL

Firebase Functions プロジェクト作成

npm インストール
npm のはじめかた - Qiita

npm でインストールするもの

npm install -g firebase-tools
npm install twitter

フォルダ準備
適当な場所にデプロイ用のフォルダを作成。

控えておくもの

  • Firebaseプロジェクトパス

ログイン
Firebase にログイン。

firebase login

プロジェクト作成
用意したフォルダに移動。

cd Firebaseプロジェクトパス

プロジェクト作成開始。

firebase init

するといろいろ質問される。

Firebase Functions 環境変数設定

キー構成は自分で分かりやすければ何でも大丈夫だと思う。

firebase functions:config:set credential.twitter.api_key="値"

設定するもの Twitch関連

  • クライアントID
  • クライアントシークレット
  • ユーザーID
  • Twitchアクセストーク
  • HMAC用シークレット

署名の確認に使用する。
10~100の文字列をパスワードのように決めておく。
Twitch で取得したアクセスシークレットや Twitter で取得したクライアントシークレットとは異なるので注意。

設定するもの Twitter関連

Twitch API サブスクリプション登録

EventSub | Twitch Developers

  1. 登録の curl コマンドを流す
  2. webhook_callback_verification_pending のレスポンスが返ってくる
  3. コールバックURLの方にリクエストが送られてくる
  4. 署名の確認をする
  5. challenge の値を返す
  6. 登録成功

という流れ。
なのでまず、コールバックURLにくるリクエスト処理(上の4, 5)を作成する。

Firebase Functions 受け取り側作成

ソース全体

署名の確認
Verify a signature | Twitch Developers

    // 署名確認
    const hmac_message =
      headers['twitch-eventsub-message-id'] +
      headers['twitch-eventsub-message-timestamp'] +
      request.rawBody;
    const signature = crypto
      .createHmac('sha256', twitchConfig.hmac_secret)
      .update(hmac_message)
      .digest('hex');
    const expected_signature_header = 'sha256=' + signature;
    const message_signature = headers["twitch-eventsub-message-signature"];
    if (message_signature != expected_signature_header) {
      functions.logger.error("Error", "Verify Signature Failed");
      response.sendStatus(403);
    }

前に設定したHMAC用シークレットを元に HMAC-SHA256 でなんやかんやしたものと、送られてきた twitch-eventsub-message-signature を比較する。
合っていなかったら403が返るようにする。

リクエスト受け取り

    if (messageType == "webhook_callback_verification") {
      // Webhhok登録認証
      response.send(request.body.challenge);
    }

ヘッダーの twitch-eventsub-message-type で判断。
webhook_callback_verification なら challenge を返す。

デプロイ

firebase deploy

ESLint 修正

サブスクリプション登録

curl -X POST -d "{\"type\":\"サブスクリプションタイプ\", \"version\":\"1\", \"condition\":{\"broadcaster_user_id\":\"ユーザID\"}, \"transport\":{\"method\":\"webhook\", \"callback\":\"コールバックURL\", \"secret\":\"HMAC用シークレット\"}}" -sS -H "Content-Type: application/json" -H "Authorization: Bearer Twitchアクセストークン" -H "client-id: クライアントID" https://api.twitch.tv/helix/eventsub/subscriptions

サブスクリプションタイプ
EventSub Subscription Types | Twitch Developers
ここでは配信開始の通知を受け取りたいので stream.online を使用する。
配信終了も受け取りたかったので stream.offline も後で同じように登録する。

Firebase 側を作成出来たら、上の curl コマンドを流す。
webhook_callback_verification_pending のレスポンスが返ってくるのを確認して、リクエスト受け取りの方も上手くいったら登録成功。

サブスクリプション確認

curl "https://api.twitch.tv/helix/eventsub/subscriptions" -H "Content-Type: application/json" -H "Authorization: Bearer Twitchアクセストークン" -H "client-id: クライアントID"

サブスクリプション一覧が取得できる。
status が enabled になっていれば大丈夫。

検証に失敗して status が webhook_callback_verification_failed になっていたり、余計なサブスクリプションを削除したい場合は以下のコマンド。

curl -X DELETE https://api.twitch.tv/helix/eventsub/subscriptions?id=サブスクリプションID -H "Content-Type: application/json" -H "Authorization: Bearer Twitchアクセストークン" -H "client-id: クライアントID"

通知受け取り処理

ヘッダーの twitch-eventsub-message-type で判断。
通知は notification。

Twitter処理

      // Twitter準備
      const client = new Twitter({
        consumer_key: twitterConfig.api_key,
        consumer_secret: twitterConfig.api_secret,
        access_token_key: twitterConfig.access_token_key,
        access_token_secret: twitterConfig.access_token_secret,
      });

      var tweet = "";
      if (subscriptionType == "stream.online") {
        // 配信開始

        // 配信タイトル取得
        const title = await getStreamTitle();

        tweet = title +
          " https://www.twitch.tv/" +
          request.body.event.broadcaster_user_login;

      } else if (subscriptionType == "stream.offline") {
        // 配信終了
        tweet = "配信おわり";
      }
      if (tweet != "") {
        await client.post("statuses/update", {
          status: tweet
        })
      }

twitch-eventsub-subscription-type にサブスクリプションタイプが入っているので、これで配信開始か終了かを判断する。

配信タイトル取得

以前のサブスクリプションでは配信タイトルが通知データ内に入っていたのだが、更新されてからは入っていないので都度取得する必要がある。

// 配信タイトル取得
async function getStreamTitle() {
  functions.logger.log("getStreamTitle Start");
  var title = "";
  try {
    const accessToken = await checkAccessToken();
    if (accessToken == "") {
      title = "配信はじめ";
      return title;
    }
    const headers = {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + accessToken,
      "client-id": twitchConfig.client_id,
    }
    const url = "https://api.twitch.tv/helix/channels?broadcaster_id=" + twitchConfig.user_id;
    const options = {
      url: url,
      method: "GET",
      headers: headers,
      json: true,
    }
    const result = await requestAsync(options);
    if (result && result.data[0].title) {
      title = result.data[0].title;
    } else {
      title = "配信はじめ";
    }
  } catch (e) {
    functions.logger.error("Error", e);
  }
  return title;
}

アクセストークン確認、更新

上の配信タイトル取得にはアクセストークンが必要だが、アクセストークンには期限が設定されているので切れている可能性がある。
その為、まず疎通確認をする。

// Twitchアクセストークン疎通確認
async function checkAccessToken() {
  functions.logger.log("checkAccessToken Start");
  var accessToken = "";
  try {
    const headers = {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + twitchConfig.access_token,
    }
    const url = "https://id.twitch.tv/oauth2/validate";
    const options = {
      url: url,
      method: "GET",
      headers: headers,
      json: true,
    }
    const result = await requestAsync(options);
    if (result && result.status != 200) {
      accessToken = await getAccessToken();
    } else {
      accessToken = twitchConfig.access_token;
    }
  } catch (e) {
    functions.logger.error("Error", e);
  }
  return accessToken;
}

期限が切れていたらアクセストークンを取得しなおす。

// Twitchアクセストークン更新
async function getAccessToken() {
  functions.logger.log("getAccessToken Start");
  var accessToken = "";
  try {
    const headers = {
      "Content-Type": "application/json",
    }
    const url = "https://id.twitch.tv/oauth2/token" +
      "?client_id=" + twitchConfig.client_id +
      "&client_secret=" + twitchConfig.client_secret +
      "&grant_type=client_credentials";
    const options = {
      url: url,
      method: "POST",
      headers: headers,
      json: true,
    }
    const result = await requestAsync(options);
    if (result && result.access_token) {
      functions.logger.log("getAccessToken Success");
      accessToken = result.access_token;
    } else {
      functions.logger.log("getAccessToken Failed");
    }
  } catch (e) {
    functions.logger.error("Error", e);
  }
  return accessToken;
}

※本当はここで取得しなおしたアクセストークンで Firebase Functions 環境変数を変更したいのだが……コマンドライン以外の変更方法が無さそうだったので断念。

バッチ作成

上のアクセストークン確認、更新だと取得したトークンを保存できないため、一度切れたらずっと都度取得し直しになってしまう。
それもどうかと思ったので、週一くらいの頻度でアクセストークンの確認をして、切れていたらコマンドを叩いて Firebase Functions の環境変数を更新するバッチを作成する。
(ドキュメントでは60日で切れるらしいので頻度はもっと低くてもいいかもしれない)
と思ったが、いろいろした結果バッチでなく exe になった。
exe の中でバッチを叩く。

コンフィグに入れるもの

  • Twitchアクセストーク
  • クライアントID
  • クライアントシークレット

確認処理

/// <summary>
/// OAuth疎通チェック
/// </summary>
/// <returns></returns>
private static async Task<bool> CheckOAuth()
{
	Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
	string twitch_access_token = config.AppSettings.Settings["twitch_access_token"].Value;

	string url = "https://id.twitch.tv/oauth2/validate";
	HttpClient client = new HttpClient();

	HttpRequestMessage request = new HttpRequestMessage(System.Net.Http.HttpMethod.Get, url);
	request.Headers.Add("ContentType", "application/json");
	request.Headers.Add("Authorization", "Bearer " + twitch_access_token);

	HttpResponseMessage response = await client.SendAsync(request);

	return response.IsSuccessStatusCode;
}

取得処理

/// <summary>
/// アクセストークン取得
/// </summary>
private static async Task<string> GetOAuth()
{
	Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
	string twitch_client_id = config.AppSettings.Settings["twitch_client_id"].Value;
	string twitch_client_secret = config.AppSettings.Settings["twitch_client_secret"].Value;

	string url = "https://id.twitch.tv/oauth2/token"
		+ "?client_id=" + twitch_client_id
		+ "&client_secret=" + twitch_client_secret
		+ "&grant_type=client_credentials";
	HttpClient client = new HttpClient();
	HttpRequestMessage request = new HttpRequestMessage(System.Net.Http.HttpMethod.Post, url);

	HttpResponseMessage response = await client.SendAsync(request);

	var responseContent = await response.Content.ReadAsStringAsync();
	dynamic data = JsonConvert.DeserializeObject(responseContent);
	string new_access_token = data.access_token;

	config.AppSettings.Settings["twitch_access_token"].Value = new_access_token;
	config.Save();

	UpdateFireBaseConfig(new_access_token);

	return new_access_token;
}

更新処理

/// <summary>
/// FirebaseのConfigを更新
/// </summary>
/// <param name="new_access_token"></param>
private static void UpdateFireBaseConfig(string new_access_token)
{
	string deployPath = @"Firebaseプロジェクトパス";
	string batPath = @"バッチを置きたい場所のパス\バッチ名.bat";

	StringBuilder sb = new StringBuilder();
	sb.AppendLine("cd " + deployPath);
	sb.AppendLine("firebase functions:config:set credential.twitch.access_token=" + new_access_token);
	sb.AppendLine("firebase deploy");

	File.WriteAllText(batPath, sb.ToString());

	ProcessStartInfo processStartInfo = new ProcessStartInfo();
	processStartInfo.FileName = batPath;
	processStartInfo.CreateNoWindow = true;
	processStartInfo.UseShellExecute = false;

	Process.Start(processStartInfo);
}

バッチファイルを作成して、起動する。
その中で Firebase Functions の環境変数を更新し、デプロイし直す。

index.js 全体

const functions = require("firebase-functions");
const Twitter = require("twitter");
const crypto = require('crypto');
const request = require('request');

const twitchConfig = functions.config().credential.twitch;
const twitterConfig = functions.config().credential.twitter;

exports.tw2tw = functions.https.onRequest(async (request, response) => {
  try {
    functions.logger.log("tw2tw Start");

    const headers = request.headers;
    const messageType = headers["twitch-eventsub-message-type"];
    const subscriptionType = headers["twitch-eventsub-subscription-type"];

    // 署名確認
    const hmac_message =
      headers['twitch-eventsub-message-id'] +
      headers['twitch-eventsub-message-timestamp'] +
      request.rawBody;
    const signature = crypto
      .createHmac('sha256', twitchConfig.hmac_secret)
      .update(hmac_message)
      .digest('hex');
    const expected_signature_header = 'sha256=' + signature;
    const message_signature = headers["twitch-eventsub-message-signature"];
    if (message_signature != expected_signature_header) {
      functions.logger.error("Error", "Verify Signature Failed");
      response.sendStatus(403);
    }

    functions.logger.log("MessageType", messageType);
    functions.logger.log("subscriptionType", subscriptionType);

    if (messageType == "webhook_callback_verification") {
      // Webhhok登録認証
      response.send(request.body.challenge);

    } else if (messageType == "notification") {
      // 通知

      // Twitter準備
      const client = new Twitter({
        consumer_key: twitterConfig.api_key,
        consumer_secret: twitterConfig.api_secret,
        access_token_key: twitterConfig.access_token_key,
        access_token_secret: twitterConfig.access_token_secret,
      });

      var tweet = "";
      if (subscriptionType == "stream.online") {
        // 配信開始

        // 配信タイトル取得
        const title = await getStreamTitle();

        tweet = title +
          " https://www.twitch.tv/" +
          request.body.event.broadcaster_user_login;

      } else if (subscriptionType == "stream.offline") {
        // 配信終了
        tweet = "配信おわり";
      }
      if (tweet != "") {
        await client.post("statuses/update", {
          status: tweet
        })
      }

      functions.logger.log("Tweet Posted", tweet);
      response.send("Tweet Posted");

    } else {
      functions.logger.error("Error", "Unknown MessageType");
      response.sendStatus(500);
    }
  } catch (e) {
    functions.logger.error("Error", e);
    response.sendStatus(500);
  }
});

// 配信タイトル取得
async function getStreamTitle() {
  functions.logger.log("getStreamTitle Start");
  var title = "";
  try {
    const accessToken = await checkAccessToken();
    if (accessToken == "") {
      title = "配信はじめ";
      return title;
    }
    const headers = {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + accessToken,
      "client-id": twitchConfig.client_id,
    }
    const url = "https://api.twitch.tv/helix/channels?broadcaster_id=" + twitchConfig.user_id;
    const options = {
      url: url,
      method: "GET",
      headers: headers,
      json: true,
    }
    const result = await requestAsync(options);
    if (result && result.data[0].title) {
      title = result.data[0].title;
    } else {
      title = "配信はじめ";
    }
  } catch (e) {
    functions.logger.error("Error", e);
  }
  return title;
}

// Twitchアクセストークン疎通確認
async function checkAccessToken() {
  functions.logger.log("checkAccessToken Start");
  var accessToken = "";
  try {
    const headers = {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + twitchConfig.access_token,
    }
    const url = "https://id.twitch.tv/oauth2/validate";
    const options = {
      url: url,
      method: "GET",
      headers: headers,
      json: true,
    }
    const result = await requestAsync(options);
    if (result && result.status != 200) {
      accessToken = await getAccessToken();
    } else {
      accessToken = twitchConfig.access_token;
    }
  } catch (e) {
    functions.logger.error("Error", e);
  }
  return accessToken;
}

// Twitchアクセストークン更新
async function getAccessToken() {
  functions.logger.log("getAccessToken Start");
  var accessToken = "";
  try {
    const headers = {
      "Content-Type": "application/json",
    }
    const url = "https://id.twitch.tv/oauth2/token" +
      "?client_id=" + twitchConfig.client_id +
      "&client_secret=" + twitchConfig.client_secret +
      "&grant_type=client_credentials";
    const options = {
      url: url,
      method: "POST",
      headers: headers,
      json: true,
    }
    const result = await requestAsync(options);
    if (result && result.access_token) {
      functions.logger.log("getAccessToken Success");
      accessToken = result.access_token;
    } else {
      functions.logger.log("getAccessToken Failed");
    }
  } catch (e) {
    functions.logger.error("Error", e);
  }
  return accessToken;
}

// 非同期リクエスト
function requestAsync(options) {
  try {
    return new Promise(function(resolve, reject) {
      request(options, function(error, res, body) {
        if (res.statusCode != 200) {
          functions.logger.error("requestAsync Status", res.statusCode);
        }
        if (error) {
          functions.logger.error("requestAsync Error", error);
        }
        resolve(body);
      });
    });
  } catch (e) {
    functions.logger.error("Error", e);
    return null;
  }
}

Twitch配信開始の通知を自動でツイートする ※旧

※2021/10/08 追記

Twitch API の Webhook サブスクリプション登録方法が更新され、WebSub 非推奨になり EventSub が推奨になったようです。
blog.twitch.tv

ということで色々試してみた結果、この記事よりかなり構成が変わりました。
なので、それについて新しく書き直しました。
wfoo13101.hatenablog.com

はじめに

この記事は、こちらの記事qiita.comを参考にしながら、分からなかったところのメモです。

こちらの記事冒頭にもあるように IFTTT を使えば手軽に配信開始の通知自体はできるんですが、遅延より何より、配信タイトルをツイートに入れたくて手作りに手を出しました。

Twitch API への Webhook 登録

記事通り Twitch Developers に登録して、
クライアントIDとトークンを控えられたらOK。

3. トークンとクライアントIDを使って API を呼ぶ

はもっと後で実行するやつなので

トークンとクライアントIDを使ってAPIを呼ぶ

curl のところはとりあえず飛ばす。

配信通知を受け取る Webhook の登録

curl はまだ入れる情報が足りないのでここは飛ばす。

Webhook 用 API の開発

Twitter Developer への登録

ちょっと面倒。

自分の場合は、英語で申請した後に日本語のメールが来て、英語と同じような内容を日本語でちゃんと書いて返信したら登録できた。
単純な通知アプリを作りたい、というのの他に、個人で使う用です、とか、勉強がてらです、とか書いたけど必要だったかは分からない。

設定変更

プロジェクトを登録出来たら、
Settings で App permissions を Write 含むように変更する。

キー、トークン取得

記事内で取得したい情報は
 consumer_key = Consumer Keys の API Key
 consumer_secret = Consumer Keys の secret
 access_token_key = Authentication Tokens の Access token
 access_token_secret = Authentication の Tokens secret
という感じ。

consumer_key と consumer_secret はプロジェクトを登録した最初に表示されたと思う。そこで控えておかないと再表示できないので、再作成しないといけなくなる。
あとの二つは Keys and tokens で作成する。こちらも作成後に表示された時に控えておかないと、再作成しないといけなくなる。

Firebase functions の設定

何も知らなかったのでここで詰まった。

npm インストール

npm のはじめかた - Qiita
 

Firebase 登録

Firebase

Functions を使うために料金プランをBlazeにする。

料金参考:
Firebaseで無料枠の範囲内のはずが課金されたのならこれが理由かも? - Qiita
 

フォルダ準備

プロジェクトのいろいろを入れるフォルダを、
適当な名前で、適当な場所に作る。

それからコマンドプロンプトを起動して、その場所へ移動(cd)。
 

Firebase CLI

Firebase CLI(firebase コマンド)を使えるようにする。

npm install -g firebase-tools

 

firebase login

Firebase にログイン。

firebase login

 

firebase init

Firebase最初の処理。

firebase init


するといろいろ質問される。

Are you ready to proceed?

(これからの処理をして大丈夫?)
 → y

Which Firebase CLI features do you want to set up for this folder?

(Firebase 機能の内、どれをセットアップする?)
 → Functions: Configure and deploy Cloud Functions のみ選択して決定

Please select an option

(オプションを選択、作成済みのプロジェクトを使うか、新たに作るかなど)
 → Use an existing project(先にプロジェクトを作成できている場合)
 → 作成済みのプロジェクト一覧が出てくるので選択して決定

What language would you like to use to write Cloud Functions?

(使用する言語は?)
 → 好きな方を選択(よく分からなければJavaScriptの方がいい?)

Do you want to use ESLint to catch probable bugs and enforce style?

(ESLint を使う?)
 → 使った方がいいと思うけど、後で設定とか調整が必要

Do you want to install dependencies with npm now?

(ついでに必要ないろいろを npm でインストールする?)
 → y


これでようやく、ある程度は下準備が出来た。


参考:
Firebaseの始め方 - Qiita
Firebase で Cloud Functions を簡単にはじめよう - Qiita
 

npm install twitter

先に作ったフォルダの中にいろいろファイルが出来ているはず。

コマンドプロンプトでその中の functions フォルダに移動(cd)。

Twitter でいろいろ出来るようになるやつを npm でインストール。

npm install twitter

 

環境変数設定

Twitter への接続情報をハードコードするのもなんなので,以下のように環境変数として登録しておきます

のコマンドを実行。
 

index.js

細かい手順については省きますが,以下のようにコードを書き……

のコードを functions 内の index.js にコピー。


コード内の配信URLが固定で書かれているので編集。

'https://www.twitch.tv/' + streamInfo.user_login

にしても良さそう。
もしくは好きな文言に変える。

firebase deploy

そしたらデプロイをしてみる。

firebase deploy

 

ESLint 修正

そのままだと大体の場合 ESLint に怒られるはずなので調整。

Parsing error: Unexpected token =>

というエラーが発生していたら、.eslintrc.js に

  parserOptions: {
    ecmaVersion: 8,
  },

を追加。
位置は多分どこでもいいけど、env の下がいい?


またデプロイをすると今度は大量に ESLint に怒られると思う。

1:27  error  Strings must use doublequote

最初のは 1行目 27文字目という意味。
この位置を頼りに修正。

Strings must use doublequote

文字列でシングルクォート(')を使ってるところをダブルクォート(")にする。

Expected linebreaks to be 'LF' but found 'CRLF'

改行コードをLFにする。

Block must not be padded by blank lines

余計な空白行を削除する。

'data' is not defined

data の前に const を追加。

Expected space(s) after "if"

if の後に空白を入れる。

Missing space before opening brace

{ の前に空白を入れる。

Missing trailing comma

行の最後にカンマを入れる。

'+' should be placed at the end of the line

文字列を繋げる + は行の最後に入れる。

There should be no space after '{'

余計な空白を削除する。

Missing semicolon

行の最後にセミコロンを入れる。

Trailing spaces not allowed

行の最後にある余計な空白を削除する。


エラーがなくなるまで修正。

Webhook 登録

これまで飛ばしていた curl の出番。

配信通知を受け取る Webhook の登録

にある以下のコマンドを編集する。

curl -X POST -d "{\"hub.callback\":\"WebhookとするURL\", \"hub.mode\":\"subscribe\", \"hub.topic\":\"https://api.twitch.tv/helix/streams?user_id=130871908\", \"hub.lease_seconds\":\"864000\"}" -sS -H 'Content-Type: application/json' -H 'Authorization: Bearer さっきのアクセストークン' -H 'client-id: さっきのクライアントID' https://api.twitch.tv/helix/webhooks/hub

分かりやすくすると、こう。

curl -X
 POST -d "{
  \"hub.callback\":\"WebhookとするURL\", 
  \"hub.mode\":\"subscribe\", 
  \"hub.topic\":\"https://api.twitch.tv/helix/streams?user_id=130871908\", 
  \"hub.lease_seconds\":\"864000\"
 }" 
 -sS 
  -H 'Content-Type: application/json' 
  -H 'Authorization: Bearer さっきのアクセストークン' 
  -H 'client-id: さっきのクライアントID' 
 https://api.twitch.tv/helix/webhooks/hub

編集部分

  • WebhookとするURL

Firebase のページを確認
デプロイが成功していたら、Function のダッシュボードに関数が表示されているはず
(コピーしたままだったら、関数名が tw2tw)
これのトリガー列で、リクエストの下に小さく表示されているURL

  • ~?user_id=130871908

Twitch のユーザーID(数字列)を取得する必要がある。

curl "https://api.twitch.tv/helix/users?login=ユーザー名" -H "Content-Type: application/json" -H "Authorization: Bearer トークン" -H "client-id: クライアントID"

ユーザー名部分には普通のアルファベットとかのユーザー名を入れる。
トークンとクライアントIDは Twitch で取得したものを入れる。
これを実行して取得できた id を user_id= の先に入れる。

Twitch で取得したトークン。

  • さっきのクライアントID

Twitch で取得したクライアントID。


以上をすべて入れて、コマンドプロンプトで実行。
返事は特に帰ってこないが、それで多分大丈夫……。


2021/4/2追記
※このWebhook(サブスクリプション)は有効期限があり、10日で切れてしまう。
https://dev.twitch.tv/docs/api/webhooks-guide

上のcurlコマンドのbatファイルを作って、タスクスケジューラーに一週間毎実行で登録してみた。


2021/5/28追記
タスクスケジューラー自体は上手くいってるっぽい。
これで通知が上手くいってない場合、OAuthの期限切れの可能性。
適当なユーザID取得を流して401が返ってきた場合は多分、そう。


適当なユーザ情報取得コマンド

curl "https://api.twitch.tv/helix/users?login=ユーザー名" -H "Content-Type: application/json" -H "Authorization: Bearer アクセストークン" -H "client-id: クライアントID"


OAuthアクセストークン取得をやり直して、
batに書いたコマンドのアクセストークンを新しいものに変える。

確認

実際の動作は試しにTwitchで配信を開始して終了させ、
Twitterに投稿されているか見る必要がある。

あとは、Firebase のページでログを見ることが出来る。

Function execution started

から

Function execution took 243 ms, finished with status code: 200 

となっていたら何事もなかった感じ。

Function execution took 411 ms, finished with status: 'crash' 

とかになっていたら何かあったので直前のログを確認。

Unhandled rejection 

とかあったら、大抵 Twitter 関連の問題。

[ { code: 187, message: 'Status is a duplicate.' } ] 

とかあるので、code や message + Twitter errorで検索。

上の code: 187 は短い間隔で連続して同じツイートをしようとしたときに怒られるやつ。

code: 89 はトークンが期限切れしたとからしいので、
Authentication Tokens の Access token & secret を再作成する。
環境変数も入れ直してデプロイしなおす。


という感じでできました、というメモでした。

【FaceRig】資料

FaceRig公式サイト

カスタムアバター作成について

facerig.com

 

Another example can be found here:

以下のリンクからサンプルデータがダウンロードできる

 

You can find all the necessary documentation using this direct link:

以下のリンクからは下記のドキュメントがダウンロードできる

「FaceRig Model and Textures Documentation v1.0」

このドキュメントに各命名規則やアニメーション一覧などが載っている

 

How to use the importer wizard:

以下のリンクからは下記のドキュメントがダウンロードできる

「How To Use ImportWizard」

このドキュメントにImportWizardの使い方が載っている

【FaceRig】困ったら

『ImportWizardのフォルダ取り込みでつまる』
  • パスの中に空白がある場合は取り除く
  • FaceRigを一度も起動してない場合は起動する
  • 「管理者で実行」でImportWizardを起動してみる

 

『ImportWizardのモデル取り込みでつまる』
  • モデルのボーン名を確認する
  • モデルのボーンを減らしてみる
  • UVマップが2つ以上あるオブジェクトが無いか確認する

 

『ImportWizardのインポートでつまる』
  • animフォルダの中身を削除してみる

 

『FaceRigでモデルが表示されない』
  • Cameraの位置と向きを確認
  • モデルの大きさを確認

 

『FaceRigでモデルの一部が表示されない』
  • Blenderでのエクスポート時に全てのメッシュを選択してからエクスポートする
  • Blenderでオブジェクトごとに「ctrl + A」で位置、回転、拡大縮小を適用する
  • 法線の向きを確認

 

『FaceRigでモデルの大きさがおかしい』
  • Blenderでオブジェクトごとに「ctrl + A」で位置、回転、拡大縮小を適用する

 

『FaceRigでモデルの向きがおかしい』
  • Cameraの位置と向きを確認
  • 軸を確認

 

『FaceRigでモデルの一部の向きがおかしい』
  • ボーンの回転を確認

 

 

【FaceRig・Blender】モデリング - ⑤エクスポート

ジオメトリ

エクスポートする際はデフォルトポーズであること

つまりポーズモードなどでポーズの変更をしていない状態であること

ポーズモードで「ポーズ」>「トランスフォームをクリア」>「すべて」

f:id:wfoo13101:20190131172444p:plain

 

アニメーションをつけていない状態にする

アーマチュアを選択し、ドープシートでアクションとのリンクを切る

f:id:wfoo13101:20190131172341p:plain

この「×」をクリック

 

COLLADAファイルでエクスポート

  • メッシュ
  • アーマチュア
  • カメラ(ボーンでカメラを作成していない場合)

をすべて選択し

「ファイル」>「エクスポート」>「Collada(デフォルト)(.dae)」を選ぶ

 

左側の設定のエクスポートデータオプションで

のチェックを入れ

  • シェイプキーを含む

のチェックを外す

f:id:wfoo13101:20190131172841p:plain

 

 

ジオメトリの命名規則

モデル名 + "Geometry"

モデル名:Test

→「TestGeometry.dae

 

この命名規則は無視可能

インポート時に自動で読み込まれなくなるだけ


アニメーション

COLLADAファイルでエクスポート

 

命名規則は自由?

公式ドキュメントのアニメーション一覧にある名前と同じものをつける?

 

ドープシートで出力したいアクションを選び、あとはモデル本体と同じようにエクスポート

【FaceRig・Blender】モデリング - ④アニメーション

必須のアニメーションはidle1のみ

モデルのインポートのみ確認したい場合はモデル本体の.daeファイルで代替できるので特に作成しなくていい

 

アニメーションは30FPSであること

レンダータブ>寸法>フレームレートで設定(画像右下)

f:id:wfoo13101:20190131171011p:plain

 

ボーンでのアニメーションであること

頂点モーフなどは不可

 

すべてのアニメーションでActionSelectorの横のFを押すようにする

f:id:wfoo13101:20190131171212p:plain

そうしないと、アーマチュアに現在リンクされているもの以外のアニメーションが保存されない

 

ジオメトリを編集しても、大きさや構造が大きく変わらなければアニメーションはそのまま使えるっぽい

 

手と腕のアニメーションはLeap Motionが無いとFaceRigで反映されないので、機器がない場合は作成しなくていい


アニメーション一覧

アニメーションがImportWizardでインポートされるとアニメーションは全てanimという名前のフォルダに入れられる

さらにanimの中で以下のサブフォルダに分けられる

 

In generalMovement anim sub-folder:

<全体の動きサブフォルダ>

Avatar_FB
Avatar_LR
Avatar_Twist
Head_LR
Head_Twist
Head_UD
idle1 ※必須

 

In EyesAndEyebrows anim subfolder:

<目と眉サブフォルダ>

LeftEye_LR
LeftEye_UD
LeftEyebrow_D
LeftEyebrow_D_ext
LeftEyebrow_U
LeftEyebrow_U_ext
LeftEyeClosed
LeftEyeSquint
LeftEyeWideOpen
RightEye_LR
RightEye_UD
RightEyebrow_D
RightEyebrow_D_ext
RightEyebrow_U
RightEyebrow_U_ext
RightEyeClosed
RightEyeSquint
RightEyeWideOpen

 

In MouthAndNose anim subfolder:

<口と鼻サブフォルダ>

CheekPuff_L
CheekPuff_R
Mouth_pursedLips_LR
Mouth_unveilledTeeth_D
Mouth_unveilledTeeth_U
MouthClosedLeft_D
MouthClosedLeft_U
MouthClosedLeft_U_visime
MouthClosedRight_D
MouthClosedRight_U
MouthClosedRight_U_visime
MouthOpen
MouthOpen_base
MouthOpen_pursedLips_LR
MouthOpenLeft_D
MouthOpenLeft_U
MouthOpenRight_D
MouthOpenRight_U
MouthTongueBase
NoseWrinker_D
NoseWrinker_U
TongueIdle
TongueOut_LR
TongueOut_UD

 

In ShouldersAndHands anim subfolder:

<肩と手サブフォルダ>

FingerL0_extFlex
FingerL1_extFlex
FingerL2_extFlex
FingerL3_extFlex
FingerL4_extFlex
FingerR0_extFlex
FingerR1_extFlex
FingerR2_extFlex
FingerR3_extFlex
FingerR4_extFlex
HandL_closeDown_LR
HandL_closeMiddle_LR
HandL_closeUp_LR
HandL_farDown_LR
HandL_farMiddle_LR
HandL_farUp_LR
HandL_solo_LR
HandL_solo_Twist
HandL_solo_UD
HandR_closeDown_LR
HandR_closeMiddle_LR
HandR_closeUp_LR
HandR_farDown_LR
HandR_farMiddle_LR
HandR_farUp_LR
HandR_solo_LR
HandR_solo_Twist
HandR_solo_UD

 

In visime anim subfolder:

<音素サブフォルダ>

visime_new_AA
visime_new_AH
visime_new_AO
visime_new_AW-OW
visime_new_CH-J-SH
visime_new_EH-AE
visime_new_EY
visime_new_FV
visime_new_IH-AY
visime_new_L
visime_new_M-P-B
visime_new_N-NG-DH
visime_new_OY-UH-UW
visime_new_R-ER
visime_new_W
visime_new_X
visime_new_Y-IY

 

Directly in anim folder:

<animフォルダ直下>

_frown
_laugh
_unveilTeeth
_wonder
cheekL
cheekR

special01
special02
special03
special04
special05
special06


これだけ作っておけばそれっぽくなるアニメーション(独断と偏見)

Avatar_FB (体全体の動き)
Avatar_LR
Avatar_Twist
Head_LR (頭全体の動き)
Head_Twist
Head_UD
idle1 (静止)※必須
LeftEye_LR (目の動き)
LeftEye_UD
LeftEyeClosed
LeftEyeSquint
LeftEyeWideOpen
RightEye_LR
RightEye_UD
RightEyeClosed
RightEyeSquint
RightEyeWideOpen
MouthOpen (口の動き)
MouthOpen_base
MouthClosedLeft_U
MouthClosedRight_U

次点

LeftEyebrow_D (眉の動き)
LeftEyebrow_D_ext
LeftEyebrow_U
LeftEyebrow_U_ext
RightEyebrow_D
RightEyebrow_D_ext
RightEyebrow_U
RightEyebrow_U_ext


参考

アニメーションの作り方

かんたんBlender講座

blenderで左右対称のポーズを作る - MRが楽しい

 

アニメーション一覧

ばけもの屋 -BAKEMONOYA- faceRig編: アニメテンプレートと一覧

【FaceRig・Blender】モデリング - ③ボーンとウェイト

ボーンの命名規則は必須

ケルトヒエラルキーに載っているボーン名にする

ただし全種類作る必要はない

(もしかしてBipを先頭につければあとの名前は自由?)

 

必須ボーン

  • Camera(カメラ)
  • BipHead(頭)
  • BipLEye(左目)
  • BipREye(右目)

の4つ

 

CameraはボーンでなくBlenderで新規ファイル作成時に既にあるCmaeraでOK

位置と向きは調整すること

顔の正面に向かい合うようにして配置する

 

全ての頂点にウェイトが設定されていること


ケルトヒエラルキー

以下、公式ドキュメントに載っているもの

下半身のボーンも含まれているが下半身はFaceRig Studioでのみ使用できる?

 

重要そうなボーン名に日本語名を追記

あとわかりにくかったので少し順番を並び替え

 

BipRoot
| BipSpine (腰)
| | BipSpine1
| | | BipSpine2
| | | | BipNeck (首)
| | | | | BipHead (頭)※必須
| | | | | | BipJaw (あご)
| | | | | | | BipBeard
| | | | | | | Bip_LipDown (中央下唇)※下唇は BipJaw(あご)の子供になっていること
| | | | | | | | Bip_LipDownInner
| | | | | | | BipLLipDown_1 (左下唇)
| | | | | | | | BipLLipDownInner_1
| | | | | | | BipLLipDown_2
| | | | | | | | BipLLipDownInner_2
| | | | | | | BipLLipDown_3
| | | | | | | | BipLLipDownInner_3
| | | | | | | BipRLipDown_1 (右下唇)
| | | | | | | | BipRLipDownInner_1
| | | | | | | BipRLipDown_2
| | | | | | | | BipRLipDownInner_2
| | | | | | | BipRLipDown_3
| | | | | | | | BipRLipDownInner_3
| | | | | | | BipTeethDown (下歯)
| | | | | | | BipTongue1 (舌)
| | | | | | | | BipTongue2
| | | | | | | | | BipTongue3
| | | | | | | | | | BipTongue4
| | | | | | | BipLCheek_4 (頬)
| | | | | | | BipRCheek_4
| | | | | | Bip_JawRef
| | | | | | BipTeethUp (上歯)
| | | | | | BipLLipCorner (左口端)
| | | | | | BipRLipCorner (右口端)
| | | | | | Bip_LipUpper (中央上唇)
| | | | | | BipLLipUpper_1 (左上唇)
| | | | | | BipLLipUpper_2
| | | | | | BipLLipUpper_3
| | | | | | BipLLipUpper_4
| | | | | | BipRLipUpper_1 (右上唇)
| | | | | | BipRLipUpper_2
| | | | | | BipRLipUpper_3
| | | | | | BipRLipUpper_4
| | | | | | BipLEye (左目)※必須
| | | | | | BipREye (右目)※必須
| | | | | | BipLEyeCornerIn (左目頭)
| | | | | | BipREyeCornerIn (右目頭)
| | | | | | BipLEyeCornerOut (左目尻)
| | | | | | BipREyeCornerOut (右目尻)
| | | | | | BipLEyelid_down1 (左下まぶた)
| | | | | | BipREyelid_down1 (右下まぶた)
| | | | | | BipLEyelid_up1 (左上まぶた)
| | | | | | BipREyelid_up1 (右上まぶた)
| | | | | | BipLEyebrow_0 (左眉)
| | | | | | BipLEyebrow_1
| | | | | | BipLEyebrow_2
| | | | | | BipLEyebrow_3
| | | | | | BipLEyebrow_4
| | | | | | BipREyebrow_0 (右眉)
| | | | | | BipREyebrow_1
| | | | | | BipREyebrow_2
| | | | | | BipREyebrow_3
| | | | | | BipREyebrow_4
| | | | | | BipLNose (鼻)
| | | | | | BipRNose
| | | | | | BipNoseRing
| | | | | | BipNoseTip
| | | | | | BipSnout
| | | | | | BipLEar (耳)
| | | | | | BipREar
| | | | | | BipLCheek_1 (頬)
| | | | | | BipLCheek_2
| | | | | | BipLCheek_3
| | | | | | BipLCheekMiddle
| | | | | | BipLCheekUpper
| | | | | | BipRCheek_1
| | | | | | BipRCheek_2
| | | | | | BipRCheek_3
| | | | | | BipRCheekMiddle
| | | | | | BipRCheekUpper
| | | | | | BipLHead_D
| | | | | | BipRHead_D
| | | | | | BipLHead_U
| | | | | | BipRHead_U
| | | | BipLClavicle (左鎖骨)
| | | | | BipLUpperArm (左上腕)
| | | | | | BipLForearm (左二の腕)
| | | | | | | BipLHand (左手)
| | | | | | | | BipLFinger0 (左親指)
| | | | | | | | | BipLFinger01
| | | | | | | | | | BipLFinger02
| | | | | | | | BipLFinger1 (左人差し指)
| | | | | | | | | BipLFinger11
| | | | | | | | | | BipLFinger12
| | | | | | | | BipLFinger2 (左中指)
| | | | | | | | | BipLFinger21
| | | | | | | | | | BipLFinger22
| | | | | | | | BipLFinger3 (左薬指)
| | | | | | | | | BipLFinger31
| | | | | | | | | | BipLFinger32
| | | | | | | | BipLFinger4 (左小指)
| | | | | | | | | BipLFinger41
| | | | | | | | | | BipLFinger42
| | | | | | | BipLWristCorr
| | | | | | | BipLWrist
| | | | | | BipLShoulder1
| | | | BipRClavicle (右鎖骨)
| | | | | BipRUpperArm (右上腕)
| | | | | | BipRForearm (右二の腕)
| | | | | | | BipRHand (右手)
| | | | | | | | BipRFinger0 (右親指)
| | | | | | | | | BipRFinger01
| | | | | | | | | | BipRFinger02
| | | | | | | | BipRFinger1 (右人差し指)
| | | | | | | | | BipRFinger11
| | | | | | | | | | BipRFinger12
| | | | | | | | BipRFinger2 (右中指)
| | | | | | | | | BipRFinger21
| | | | | | | | | | BipRFinger22
| | | | | | | | BipRFinger3 (右薬指)
| | | | | | | | | BipRFinger31
| | | | | | | | | | BipRFinger32
| | | | | | | | BipRFinger4 (右小指)
| | | | | | | | | BipRFinger41
| | | | | | | | | | BipRFinger42
| | | | | | | BipRWristCorr
| | | | | | | BipRWrist
| | | | | | BipRShoulder1
| | | | BipLShoulder2
| | | | BipLShoulder3
| | | | BipRShoulder2
| | | BipLShoulder4
| | | BipRShoulder3
| | | BipRShoulder4
| BipLThigh
| | BipLCalf
| | | BipLFoot
| | | | BipLToe
| BipRThigh
| | BipRCalf
| | | BipRFoot
| | | | BipRToe
Camera (カメラ)※必須