Max 8上でニコニコ動画のコンテンツを利用する方法を提供します。具体的には、シリーズ全体で次のノウハウを提供することを目的とします。
jit.movie
で再生する本章では、MaxでNode.jsオブジェクトを動かしてニコニコAPIを利用するテストケースを通じて、Node.jsとhttpリクエストについて理解を深めていきます。ただし、このページを参考にしただけなので、リンク先の技術内容を理解できる人は、そちらを読んだ方が早いです。
本記事では、ニコニコ動画のAPI利用方法について述べますが、API利用を正当化するものではありません。ニコニコAPIはわりと非公開なので、利用は自己責任です。サーバーに負荷をかけすぎると、アカウントがBANされるかもしれません。私は開発中、とある動画を一切視聴できない状態になってしまいました。動画ごとの出禁システムがあることにたまげました。場合によってはもっと怖いことが起きるかもしれないので、なるべくテストしてから動かしてみることをお勧めします。
一連の記事はニコニコのコンテンツをMax 8(以下、単にMax)上で利用したい人への情報提供が目的です。今回は、ニコ生のコメントをMaxの制御に使えるように取り出します。
本章で取り扱う範囲は、以下の通りです。
本章からは、いよいよNode.jsが登場します。ただし、私自身が書いたコードはほとんどなく、参考文献からの、スクラップブックコーディングにすぎません。でも似たようなことをしたい人の時短になるように共有させてください。
今回はjavascriptコードを書いていきます。Maxに付属しているエディタは、非ASCII文字の取り扱い、正規表現(/.../)やコメント(/*...*/)の範囲の認識にガバがあるので使いづらいです。ぜひ外部テキストエディタを使用することをお勧めします。私は、Visual Studio Codeを使用しています。でも何が一番いいエディタかは知りません。Max ツールバーから、Options > Preference > Interface > External Text Editor で使い慣れたエディタを登録してください。
Node.jsとnpmはどちらもMaxに入っていますので、あえてインストールする必要はありません。でも、Maxの外でもNode.jsやnpmを使いたくなるかもしれません。そういう人のためにインストール手順を解説しているサイトをご紹介します。(参考サイト:Win Mac)無事インストールが完了したら、コンソールから使えるようになります。
新規パッチャーを作り、そのまま保存してください。そして、保存したパッチャーのあるフォルダーにnicotest.jsという名前のテキストファイルを作成してください。
次に、Maxでnode.script nicotest.js
というオブジェクトを作成し、パッチャーをロックしてから、ダブルクリックして下さい。先ほどの手順で外部エディタを登録した人は、外部エディタが起動すると思います。
起動したエディタで、Hello, World!プログラムを書いていきましょう。せっかくなので、コンソールのほかに、パッチャー上のメッセージボックスにもHello World!というメッセージを送りたいですね。そんなプログラムは次のようなものです。(例1)
const maxApi = require('max-api');
function hello() {
maxApi.post('Hello, World!');
maxApi.outlet('Hello, World!');
};
maxApi.addHandler('bang',hello);
最初の行は、max-apiという名前のモジュールをmaxApi
という名前で使いますよ、という宣言文です。このmax-apiモジュールはNode.jsで処理した結果をMaxに渡したり、Maxのリソースにアクセスするのに使います。要するにほぼ必須です。
次のfunction
から始まる文はhello
という名前の関数を宣言しております。()
は何も引数をとらないことを示しています。関数の実行内容は{と}の間に書きます。上の例の実行内容は、maxApi.post('Hello, World!');
およびmaxApi.outlet('Hello, World!');
ということになります。maxApi.post('Hello, World!');
はmax-apiモジュールのメソッドpost
を呼び出します。post
は引数をコンソールに出力するメソッドです。
maxApi.outlet('Hello, World!');
は、node.script
の第1アウトレットから、"Hello, World!"というメッセージを出力します。
最後の行で呼び出しているメソッドaddHandler
は、node.script
に対して発生するイベント(メッセージの入力)に応じて、"関数を実行する機能"(イベントハンドラ)を作るメソッドです。どのイベントに反応させるか、はmaxApi.addHandler
の第一引数で指定します。上の例では、Maxでおなじみのbang
に反応するようにしてます。実行したい関数は、addHandlerの第二引数に渡します。上の例では、hello
を渡しているので、bang
イベントに反応して、hello()
を実行する("bang
イベントで発火する"と呼びます)ようになります。
javascriptでは、hello()
関数の宣言の代わりに、無名関数(function ())を作って変数hello
に入れるように書きかえられます。(例2)
const maxApi = require('max-api');
const hello = function() {
maxApi.post('Hello, World!');
maxApi.outlet('Hello, World!');
}
maxApi.addHandler('bang',hello);
また、変数に入れるのを省略すると、(例3)
const maxApi = require('max-api');
maxApi.addHandler('bang',function() {
maxApi.post('Hello, World!');
maxApi.outlet('Hello, World!');
}
);
のようにも記述可能です。javascriptに慣れていない人は面食らうかもしれませんが、この記述方法は、インターネットに公開されているたくさんのサンプルコードで頻出する形です。また、function(){...}
の代わりに()=>{...}
と書くこともできます。
※厳密に言えば、上記3つの例にも微妙に違いがあります。それは、hello()
を呼べる場所です。最初の例では、宣言文の前であっても呼び出せます(巻き上げといいます)。2つ目の例では、変数hello
が宣言される前に呼び出すことはできません。3つ目の例では外から呼び出せません。いろんなところで呼べる方が便利といえば便利ですが、意図しないところで呼べてしまうことが弊害になりえます。
配置図
node.script
に対して、script start
, script stop
, 〇
をインレットにつなぎます。第1アウトレットにメッセージボックス
、第2アウトレットに node.debug
をつなぎます。
準備は整いましたが、この状態で〇
をクリックしても何も起こりません。bang
を受け付けるためには、先にNode.jsを動かす必要があります。Node.jsの起動には、script start
のメッセージを送信する必要があります。script start
送信後、デバッグ画面の背景が緑色になれば正常に動作したことになります。しかし、まだ何も出力されません。javascriptは、node.script
がbangを入力されたら関数を実行するように、addHandlerというメソッドを呼び出したあと、処理が終わりました。プログラム処理は終わったNode.jsは何もすることがないのでスリープ状態にありますが、イベント発生を待受けています。この状態で
〇
をクリックすると、出力に"Hello, World!"が出てきます。Node.jsを止めるには、script stop
のメッセージを入力します。script stop
メッセージを受け取ったNode.jsはそれ以降のメッセージの入力を受け付けませんし、javascriptの実行も止まります。ある特定のイベントハンドラだけを止めたい場合は、removeHanlder()
およびremoveHandlers()
というメソッドで削除することができます。
上記のNode.jsオブジェクトは、大仰なメッセージボックスでしかなく、何の面白みもありません。そこで、Max側からの入力に応じて処理を行い、結果を出力するようにしていきます。
それでは、Maxから二つの数字を受け取って合計値を出力するようにしてみましょう。
const maxApi = require('max-api');
maxApi.addHandler('list',function(x, y) {
maxApi.outlet(x + y);
}
);
'list'
はメッセージ文ではなく、Maxのリストメッセージを入力された時の動作を指定します。メッセージが4 5
であれば、9
が出力されます。なお、2つ目の要素が文字列でも動いてしまいますので、入力されるリストは必ず2つの数字になるように設計する必要があるかもしれません。一方、先頭の要素が文字列である場合、そのメッセージをイベント名と解釈するため、対応するイベントハンドラが呼ばれます。('list'
のイベントハンドラは呼ばれません)つまり、好きに決めた名前のイベントを発火させることで、望みの処理をするようにできます。なお、その場合、引数としてイベントハンドラに渡されるリストは先頭の要素(つまりイベント名)が削られます。
const maxApi = require('max-api');
maxApi.addHandler('add',function(x, y) {
maxApi.outlet(x + y);
}
);
maxApi.addHandler('multiply', (x, y) => {
maxApi.outlet(x * y);
}
);
上記のようにすると、add 4 5
メッセージの入力によりadd
イベントが発火して、9
が出力されます。multiply 4 5
とした場合はmultiply
イベントが発火し、20
が出力されます。この場合も、add
やmultiply
に続いて、2つの数字を持つように設計する必要があるかもしれません。"スプレッド構文"を使うことで、要素の数がいくつであっても対応できるイベントハンドラを作れます。必要になった時に調べてみてください。また、例示のためfunction(x,y){}
の書き方と(x,y)=>{}
の書き方を混在させていますが、使い分ける意味は何もないのでマネしないほうがいいと思います。
うまくいきましたか?私の解説は拙いので、詰まってしまったらjavascriptの解説サイトを参考にしてください。
ニコニコと関係のない練習が長くなってしまいました。次からはいよいよニコニコAPIの利用を試すことになります。まずは、簡単なAPIから利用して練習します。ニコ生コメント取得に至るまではまだまだ先が長いですが、頑張りましょう。
ログインせずに使えるニコニコAPIのうち、ユーザー名取得用APIを試します。APIリストによると、ユーザーの名前を取得するには、http://seiga.nicovideo.jp/api/user/info?id={user_id}というURLにアクセスすればよいようです。まずは、ブラウザを使って、{user_id}を自分のユーザーIDなどで置き換えたURLにアクセスしてみてください。すると、XMLの木構造が表示されます。上に書いてあるメッセージはブラウザ側で追加されているものです。<nickname>あなたのユーザー名</nickname>のような表示が確認できます。
それでは、ユーザーIDをMaxから入力したら、node.scriptがAPIをたたいて、Maxにユーザー名が出てくる、という仕組みを作っていきましょう。
Node.jsにnpmとaxiosをインストールします
Node.jsでAPIにアクセスするためには、httpクライアントが必要です。今回、httpクライアントとしてaxiosモジュールを使用します。axiosは管理ツールnpmでインストールしますので、まず、npmをインストールする必要があります。script npm install
メッセージをnode.scriptに送って、npmをインストールしてください。デバッグ画面で、インストール完了報告が出たら、同様にscript npm install axios
メッセージでaxiosをインストールしてください。
npmとaxiosのインストールがすんだら、コードを書いていきましょう。getname
で発火するイベントハンドラを作ります。同時に、数値を入力してユーザーIDとしてイベントハンドラに渡します。
const maxApi = require('max-api');
const axios = require('axios');
const usernameURL = 'https://seiga.nicovideo.jp/api/user/info?id=';
maxApi.addHandler('getname', async (id) => {
const response = await axios(usernameURL + id).catch(() => { return 'ERRORNAME'; } )
maxApi.outlet(response.data);
}
);
axiosモジュールも、使うために宣言が必要です。通例としてモジュールは同名変数に入れますが、別の名前でも問題はありません。axios()
というメソッドは、文字列だけを渡すとそれをURLと解釈してサーバーにGETリクエストを送ります。返り値は、サーバーからのレスポンスです。axiosのreadmeによれば、レスポンスの構成は以下のようなオブジェクトです。
{
data: {},
status: 200,
statusText: 'OK',
headers: {},
config: {},
request: {},
}
本文であるXMLデータは上記オブジェクトのメンバのうち、data
に入りますので、response.data
をmaxApi.outlet()
で出力しましょう。
これまでのメソッドは、まず失敗しないものばかりでしたが、axios()
は失敗する可能性があります。URLが古いとか、ネットワーク回線の調子が悪いとか、いろんなことが原因になります。例外処理が必要なので、axios()
の後ろにcatch()
というものをつけます。catch
の中には、axios()
が失敗したときだけ、代わりに実行する処理を書きます。
イベントハンドラの前にasync
というキーワード、axios()
の前に await
というキーワードが登場してきました。async
に指定した関数内で、await
指定したメソッドや関数は、その処理が完了するまで、次の処理に進まなくなります。逆に言えば、axios()
はそのままだと完了を待たずに次の処理に進んでしまいます(非同期処理といいます)。どうしてそうなっているかは、本記事では解説できませんが、ともかく、await
がない場合、const response
がaxios()
の返り値を受ける前にmaxApi.outlet()
が呼ばれてしまいます。つまり、何も出力されずに処理が先に進みます。axios()
によるリクエストに対するレスポンスは”いつかは”帰ってくるのですが、そのころには、もうすでにmaxApi.outlet()
の処理が終わっているので、後の祭りです。async
と await
を書くことで、const response
に返り値が入ってから、maxApi.outlet()
が呼ばれることを保証することができます。同様のことをするための方法は他にもありますが、は”他の方法より簡単に書くこと”を目的にasync
と await
が近年追加された経緯があるように、断然書きやすいし、読みやすいです。axios
のほかにもたくさんの非同期処理があるので、これが書けるようになると楽しいです。
では、動かしてみましょう。なお、ユーザーID:2の人は、戀塚さんです。
名前は出てきたけど……なんか変?
response.data
全体を出力しているため余計なものまでメッセージボックスに出力されています。文字が欠けているように見えますが、メッセージボックスの内容を編集しようとすると正常な表示になりますので、ちゃんとデータは出力できたようです。名前だけを抜き出すため、response.data
(文字列)から特定の文字列を検索します。
const maxApi = require('max-api');
const axios = require('axios');
const usernameURL = 'https://seiga.nicovideo.jp/api/user/info?id=';
maxApi.addHandler('getname', async (id) => {
const response = await axios(usernameURL + id);
const matched = response.data.match(/<nickname>(.+)<\/nickname>/);
maxApi.outlet(matched[1]);
}
);
文字列にmatch()メソッドを実行すると、正規表現(/<nickname>(.+)<\/nickname>/)にヒットした文字列が格納された配列が返ります。このとき、正規表現に()で囲まれた部分が1か所以上ある場合、その部分を抜粋した文字列が、順番に配列に追加されていきます。正規表現において"."はすべての文字、"+"は1回以上の繰返しです。"\/"の二つの文字は"/"という文字そのものを意味します。("/"は正規表現の符号なので、そのまま書けません)
上のコードを実行すると、matched[0]に"<nickname>hoge</nickname>"が、matched[1]に"hoge"が入ります。matched[1]だけ出力すれば、ユーザー名だけをMaxの処理に渡すことができます。
次は、サーバーに情報を渡してレスポンスを受け取るために、POSTプロトコルで通信してみます。段々面白くなってきましたね!
Node.jsでニコニコにログインします。ログインにはIDとパスワードが要りますが、IDとパスワードをjavascriptコード本体に書いてしまうのは、かなりよろしくないので、Maxのdictオブジェクトに一時保存することにしましょう(これも、セキュリティ的によろしくないですけど)。dictの名前は、accountinfoとし、mail_tel, password, user_sessionというキーと値のペアを持つようにします。mail_telはログインID、passwordはログインパスワードです。user_sessionは後で使いますので、空の文字列を入れておいてください。dictオブジェクトの中身は以下のようになります。末尾のカンマを忘れないようにしてください。ただし、最後のメンバにはカンマをつけないでください。ちょうどこんな感じです:
{
"mail_tel": "example@example.com",
"password": "_!_pass1234$",
"user_session": ""
}
次は、ログインするためのコードを書いていきます。ログイン用のAPIのURLは、https://account.nicovideo.jp/api/v1/loginです。このURLに、ログインに必要なデータを送信します。先ほどの例同様、axiosを使います。
const maxApi = require('max-api');
const axios = require('axios');
const loginURL = 'https://account.nicovideo.jp/api/v1/login';
maxApi.addHandler('login', async () => {
const accountinfo = await maxApi.getDict('accountinfo');
const response = await axios({
url: loginURL,
method: 'post',
data: {
mail_tel: accountinfo.mail_tel,
password: accountinfo.password
},
});
maxApi.outlet(response.headers);
}
);
maxApi.getDict()
は、パッチャー上にあるdictオブジェクトをとってきて、中身をjavascriptオブジェクトとして返すメソッドです。先ほど、IDとパスワードを、それぞれmail_telとpasswordとしてdictに書いておきましたので、上の例では、それぞれaccountinfo.mail_tel
, accountinfo.password
で得ることができます。
POSTプロトコルでは、axios()
にはURLのほか、プロトコル指定用文字列('post'
)と、送信データであるオブジェクトを一緒に渡す必要があります。これらはそれぞれ、url, method, dataというキーの値としてもつオブジェクトで渡します。
うまくいくと、サーバーからのレスポンスが、const response
に格納されます。ログイン情報は、response.headers
に入っているので、最後に、response.headers
をnode.scriptから出力します。なお、axios()
によって得られるレスポンスのヘッダはjavascriptオブジェクトなので、そのままMaxのdictオブジェクトに入力できます。
出力先にdictオブジェクトを置きます。
ちょうどこんな風にすると、ヘッダーをheadersという名前のdictに保存することができます。maxApi.getDict()
はパッチャーにあるdict全てにアクセスできるので、配線する必要はありません。先ほどのコードの最後をmaxApi.outlet()
ではなくmaxApi.setDict()
を用いて、maxApi.setDict("headers",response.headers);
と書き換えても同じです。maxApi.setDict()
は既存のdictオブジェクトの中身を置き換えるメソッドで、第一引数はdictの名前、第2引数はjavascriptオブジェクトです。maxApi.setDict()
を用いる場合も、node.scriptとdictを配線する必要はありません。
script start
で起動してから、login
を一度だけ押してみてください。そのあと、ログイン情報のページにアクセスして、ログイン出来たかどうかを確認してください。"有効なアクセス"欄に"ブラウザ"によるログインがあれば成功しています。失敗した方は、addHandler()
のイベント名とイベントメッセージの組合せ、ログインAPIのURL、dictの名前(accountinfo
;)、メールアドレスとパスワードの組合せ、キー名(mail_tel
, password
)、axios()
に渡すオブジェクトと、その中のdata
オブジェクトの内容が全部正しいかをチェックしてください。
ニコニコのサーバーは、アクセスしてきたユーザーがログインしているかどうかを、クライアントから送信されるクッキーの中の、"user_session"の値を見て判定しています。この値は、ログイン時のレスポンスヘッダーの、set-cookieに書かれていて、ブラウザはこれを記録することで、ログイン状態を保っています。
上記コードでログインが成功した後、dict headersをのぞいてみると、サーバーからのレスポンスのヘッダーが格納されています。しかしながら、set-cookieに"user_session"の値は見つかりません。ログインは成功しているのに、どういうことなの……。
ニコニコのログインAPIは、ログイン成功時のステータスコード(404 Not Foundとか500 Internal Server Errorとかの数字です)として、302
を返します。これはリダイレクトを求めるレスポンスです。axiosはデフォルトでは、5回までのリダイレクトに従いリダイレクトが終わった後のページのレスポンスを返します。つまり、ログイン直後の302
を返してきたレスポンスのヘッダーは捨てられています。またデフォルトでは、最終的にレスポンスのステータスが200番台でないと通信が失敗したとしてエラーを吐きます。これを回避するには、リダイレクトを禁止し、ステータスコード302
でも通信成功とする必要があります。axios()
にこんな注文を付けましょう。
const maxApi = require('max-api');
const axios = require('axios');
const loginURL = 'https://account.nicovideo.jp/api/v1/login';
maxApi.addHandler('login', async () => {
const accountinfo = await maxApi.getDict('accountinfo');
const response = await axios({
url: loginURL,
method: 'post',
maxRedirects: 0,
validateStatus: (status) =>{ return status == 302;},
data: {
mail_tel: accountinfo.mail_tel,
password: accountinfo.password
},
});
maxApi.outlet(response.headers);
}
);
axios
に渡すオプションオブジェクトに、maxRedirects
とvalidateStatus
が増えています。maxRedirects
はその名の通り、リダイレクトに従う回数です。validateStatus
はサーバーからレスポンスを受け取るたびに評価される関数で、サーバーから受け取ったステータスコードを関数に渡して、true
が帰ってきたら通信成功と判定します。ここでは、ステータスコード302
ならtrue
、それ以外はfalse
とするため、return status == 302
としました。しかし、通信成功と判定されても、リダイレクトには従ってしまうので、結局 maxRedirects: 0
として、リダイレクトを禁止する必要があります。
コードを書き換えたのち、もう一度ログインしてみてください。今度はset-cookieに"user_session=deleted; ..." や "user_session=user_session...." という値が確認できると思います。このうち、後者が有効なuser_sessionですので、これを保存します。
const maxApi = require('max-api');
const axios = require('axios');
const loginURL = 'https://account.nicovideo.jp/api/v1/login';
maxApi.addHandler('login', async () => {
const accountinfo = await maxApi.getDict('accountinfo');
const response = await axios({
url: loginURL,
method: 'post',
maxRedirects: 0,
validateStatus: (status) =>{ return status == 302;},
data: {
mail_tel: accountinfo.mail_tel,
password: accountinfo.password
},
});
const cookies = response.headers['set-cookie'].join();
const user_session = cookies.match(/user_session=user_session.*?;/)[0];
maxApi.updateDict('accountinfo', 'user_session', user_session);
}
);
response.headers['set-cookie']
は文字列をを要素としてもつ配列であり、このままでは文字列を検索しにくいため、join()
で全要素をひとつながりの文字列にしてから、正規表現でマッチングしています。続いて、accountinfo
の"user_session"の値をマッチした文字列(user_session)で上書きするため、maxApi.updateDict()
を呼んでいます。
axiosをインストールしたときと同じ要領で、netと、cheerioというモジュールをインストールします。script npm install net メッセージおよびscript npm install cheerio メッセージを node.script に入力して下さい。
ニコ生コメントをNode.jsから取得する方法は、このページがお手本となります。また、新配信システムに対応する方法は、こちらのページでとても詳しく解説されています。後者のページの方法は筆者がまだ試していないため、今回は、旧配信サーバーにアクセスしてコメントを取得することにします。async と await を使って参考サイトと同じ動作をするように目指したコードは以下の通りです。node.scriptオブジェクトに対し、'login'メッセージでログインして、'getlv lv****'で生放送ID:lv****のコメントを受信できるようになります。※例外処理はエラーを特定するために便利ですので、適宜追加してください。
const maxApi = require('max-api');
const axios = require('axios');
const net = require('net');
const cheerio = require('cheerio');
const loginURL = 'https://account.nicovideo.jp/api/v1/login';
const getplayerstatusURL = 'http://live.nicovideo.jp/api/getplayerstatus/';
const viewer = new net.Socket();
maxApi.addHandler('login', async () => {
const user_session = await nicolive.getsession();
if(user_session){return;}
await nicolive.login();
});
maxApi.addHandler('getlv', async (lvid) => {
const user_session = await nicolive.getsession();
if(!user_session){return;}
const thread = await nicolive.fetchThread(lvid, user_session);
nicolive.view(thread);
});
const nicolive = {
login: async() => {
const accountinfo = await maxApi.getDict('accountinfo');
const response = await axios({
url: loginURL,
method: 'post',
maxRedirects: 0,
validateStatus: (status) => { return status == 302; },
data: {
mail_tel: accountinfo.mail_tel,
password: accountinfo.password
},
});
const cookies = response.headers['set-cookie'].join();
const user_session = cookies.match(/user_session=user_session.*?;/)[0];
maxApi.updateDict('accountinfo', 'user_session', user_session);
},
fetchThread: async(lvid, user_session) => {
const response = await axios({
url: getplayerstatusURL + lvid,
headers: {Cookie: user_session}
});
const $ = cheerio.load(response.data);
const port = $('getplayerstatus ms port').first().text();
const addr = $('getplayerstatus ms addr').first().text();
const thread = $('getplayerstatus ms thread').first().text();
return {port: port, addr: addr, thread: thread};
},
view: async(thread) => {
viewer.removeAllListeners();
viewer.destroy();
process.nextTick(() => {
viewer.connect(thread.port, thread.addr, () => {
viewer.setEncoding('utf-8');
viewer.write('<thread thread="'+thread.thread+'" res_from="-5" version="20061206" />\0');
});
viewer.on('data', (data) => {
const $ = cheerio.load(data);
$('chat').each();
maxApi.outlet(texts);
});
});
},
getsession: async() => {
const dict = await maxApi.getDict('accountinfo');
return dict.user_session;
},
};
ログイン処理は、nicolive
のメソッドにそっくり移動しました。ただし、user_session
がdict
に存在する場合は、ログインをスキップします。
まず、user_session
がdict
から読めない場合は処理を中断します。つまり、何も起こりません。そうでない場合、fetchThread()
というメソッドを呼び出して、完了後、次いでview()
というメソッドを呼び出して、終わりです。
生放送IDとuser_session
を引数にとります。参考サイトで解説されているように、getplayerstatus APIにuser_session
を渡して、ポート、アドレス、スレッドなどの情報をもらう必要があります。レスポンスから情報を切り出しているのですが、何をしているかは次節で説明します。
プログラム冒頭部のconst viewer = new net.Socket();
では2つのマシン間でソケット通信をするためのnet.Socketというクラスのインスタンスを作って(new
)います。net.Socketに発生するいろいろなイベントに対して、on()
でそのときのイベントハンドラ(イベントリスナー)を設定できます。'connect'
:接続したときに発生するイベント。エンコードをutf-8に設定し、サーバーに指定した文字列を送信(write)します。内容は、スレッドを開いて、最新コメント5つを送るようにサーバーに催促するものです。'data'
:データを受信すると発生するイベント。ハンドラー渡す引数data
は、受信したデータそのものであり、これをMaxの制御に使えるように、次節で説明するcheerioというモジュールによってコメント本文だけを抽出し、maxApi.outlet()
で出力します。
サーバはコメントをXMLデータとして送信します。HTMLやXMLによるレスポンスは、その構造を解釈することにより、コメントの種類、ユーザー情報、時間情報などのたくさんの情報を読み取ることができます。具体的には、下のような構造のデータが送られてきます。各パラメータの意味合いは、新コメントシステムと同じだと思います。
<chat thread="0000000000" no="810" vpos="143000" date="1597328767" date_usec="114514" mail="184" user_id="Th1siSJu5tAnExaMp1eId" anonymity="1">オッツオッツ</chat>
chat要素しかないので、無茶苦茶単純ですね。<chat>~</chat>の中身を指定していきます。
cheerioモジュールは、操作する対象となる要素をかなり細かく指定できるので大変便利です。今回のプログラムでは、正規表現を使った文字列抽出だけでも出来ますが、cheerioを使う方が楽だと思います。cheerio.load()
は文書の構造を解釈します。$
はデータをロードしたcheerioオブジェクトが入ります。$
にメソッドを実行することで、抽出する条件を設定します。$('xxx')
とすると、xxxという名前の要素を(全部)指定し、それに対してtext()
を実行すると文字列化します(複数あっても、連結した文字列となります)。また、first()
は見つかった最初の要素だけ指定します。また、each()
は、要素一つ一つについて、引数として受け取った関数を評価します。上のコードでは、i
は要素のインデックス番号(0から始まる)、el
は要素そのもの(オブジェクト)であり、$(el)
はその要素の指定となります。今回の関数では、要素を一個ずつをテキスト化して出力するために、each()
を用いて、それぞれの要素をテキスト化しています。'connect'
イベント発火時は一度に5つ送られ、その後の'data'
イベント発火時は1つずつデータが送られてきますが、上記のようにすることで要素がいくつあっても対応できます。
node.script
オブジェクトとして動かすmax-api
モジュールで、Maxに出力する(outlet()
, post()
)node.script
に対する入力に応じて何か処理をさせるときは、addHandler()に関数を渡すaxios
で、ニコニコAPIにアクセスできるnet
のnet.Socketでニコ生コメントサーバーと通信できるcheerio
で、HTMLもしくはXML文書を解釈して操作できる"all" (MAXAPI.MESSAGE_TYPE.ALL)
:全てのメッセージ"bang" (MAXAPI.MESSAGE_TYPE.BANG)
:bangメッセージ"dict" (MAXAPI.MESSAGE_TYPE.DICT)
:辞書メッセージ"number" (MAXAPI.MESSAGE_TYPE.NUMBER)
:数値メッセージ"list" (MAXAPI.MESSAGE_TYPE.LIST)
:リストメッセージなお、"all"のイベントハンドラは他のイベントハンドラが実行されるかどうかに関わらず、毎回実行される。そのとき、また、イベント名を含む入力リストの先頭に、0か1が追加されたリストがイベントハンドラに渡される。先頭が0の時は他のハンドラが実行されないことを示し、先頭が1の時は、しかるべきハンドラが実行されることを示す。
コードを読みやすく書こうと思うとスタイル指定を根性で入力する必要があり、しんどいです。一般ブロマガ投稿者にもCSSは解禁して欲しいですね。