Windows 7 で OpenSSL を 使えるようにする

ちょっと OpenSSL が 必要なことってありませんか?(いや、そうそう無いか…)
普段使用している環境は Windows で、OpenSSL を 簡単に使うことができません. ちょうど OpenSSL が 必要な困った事案が発生、また環境構築にはまったので記録を残しておきたいと思います.

作業環境

  • Windows 7 64bit
  • Microsoft Windows SDK v7.1
  • Strawberry Perl 5.24.0.1 64bit
  • OpenSSL 1.0.2j LTS

ビルド環境の準備

OpenSSL は バイナリを配布していません. 自分でビルドしないと使えません. インターネットは広いので親切な方がビルドしたものを配布してくれていますが、今回は自前でビルドする環境を用意したいと思います.

C++ コンパイラ の 用意

まず C++ の コンパイラが必要です. “April 2005 Platform SDK is equipped – OpenSSL/INSTALL.W64” と なっていますが、”古い SDK が必要な場合は、以下の MSDN サブスクリプションのダウンロードをご検討ください – JAPAN Platform SDK(Windows SDK) Support Team Blog“ とのことで、有償です…
というか、ちゃんと現在の OS に 合わせた SDK を 使う必要があるので、Windows 7 の SDK を 取ってきます.
こちらから Microsoft Windows SDK for Windows 7 and .NET Framework 4 からダウンロードしてインストールします.
最低限のインストール・オプション は 以下となります.

  • Windows Headers and Libraries
  • Visual C++ Compilers
  • Microsoft Visual C++ 2010 (Redistributable)

Perl の 用意

続いて、Perl が 必要となります. これまた Windows ユーザ としては困るところです…
ドキュメントでは “You can run under Cygwin or you can download
ActiveState Perl – OpenSSL/INSTALL.W64” となっています. [ActivePerl(http://www.activestate.com/activeperl)] は 沢山お世話になり今回もお世話になるところですが、最近は Strawberry Perl なるものもあるようです.

cpanppm などの 違いがあったりするようですが、今回 Strawberry Perl を 選択したのは、PortableZIP edition が あったからになります.
OpenSSL の Configure を 実行したいだけなので、Zip を 解凍するだけで使えるはとてもありがたいです. できればインストーラであれこれ入れてほしくないし、フォルダの削除だけで全て無かったことにできるのは助かります. ということで、Strawberry Perl で 行きます.

ウェブサイトからダウンロードして解凍して終了です.
今回は C:\Develop\sdk\strawberry-perl-5.24.0.1-64bit-portable に 解凍したものとします. README.txt に 書かれていますが、スペースや日本語が含まれないディレクトリにするとのことです.

参考情報
Perl の 選択肢については、以下のサイトを参考にさせて頂きました. すばらしい情報ありがとうございます!

OpenSSL を ビルド

OpenSSL の ソース を ダウンロードします. OpenSSL の ウェブサイト から取得しますが、Web サーバ で 使うわけではないので、安定版ということで 今回は LTS(Long Term Support) の openssl-1.0.2j.tar.gz を 選択しました.

ダウンロードしたアーカイブを解凍します.
今回は C:\Develop\tool\openssl-1.0.2j に 解凍したものとします.

続いて Windows SDK 7.1 Command Prompt を 起動します. (通常 の コマンド プロンプト ではないことに注意)

1
2
3
4
5
6
7
8
9
10
11
c:\Develop\tool\openssl-1.0.2j> set PATH=%PATH%;c:\Develop\sdk\strawberry-perl-5.24.0.1-64bit-portable\perl\bin
c:\Develop\tool\openssl-1.0.2j> perl Configure VC-WIN64A
c:\Develop\tool\openssl-1.0.2j> ms\do_win64a
c:\Develop\tool\openssl-1.0.2j> nmake -f ms\ntdll.mak
c:\Develop\tool\openssl-1.0.2j> cd out32dll
c:\Develop\tool\openssl-1.0.2j> ..\ms\test
...(省略)
passed all tests

c:\Develop\sdk\openssl-1.0.2j\out32dll>openssl version
OpenSSL 1.0.2j 26 Sep 2016

無事、ビルドできました!

鍵生成 や 自己署名証明書発行 の テスト

OpenSSL の 利用にあたっては、通常 の コマンド プロンプト で 大丈夫です.
実行に当たっては環境変数の設定が必要なものがあったりしますので注意が必要です. (ちゃんとインストールしようよということでもあるのですが、ちょっと使うだけだから… と 言い訳してみる)

1
2
3
4
5
6
7
8
9
c:\Temp> set PATH=%PATH%;c:\Develop\tool\openssl-1.0.2j\out32dll

c:\Temp> set RANDFILE=%TEMP%\.rnd
c:\Temp> set OPENSSL_CONF=c:\Develop\tool\openssl-1.0.2j\apps\openssl.cnf

c:\Temp> openssl genrsa -aes256

c:\Temp> openssl genrsa -out test.key 4096
c:\Temp> openssl req -x509 -new -key test.key -out test.pem

はまったところ

Visual C++ Compilers が 選択できない

.NET 4 (4.x ではなく 4) が 必要になります. しかも、新しいバージョンが入っているとインストールできないというトラップがありました… 新しいバージョンが入っている場合はアンインストールして、古いバージョンから順番に入れなおす必要があります.
.NET の バージョンについては、こちら Tech TIPS:.NET Frameworkのバージョンを整理する - @IT が 詳しいです.

インストーラは正常終了しているのに、インストールされていない

何が起こったのかよくわからない事象で、見事にどはまりしました orz
インストーラは正常終了したように見えているのに、肝心のプログラムが入っていない状況が起こりました. 1 GB の インストールにしては、やたら早く終わったなぁというのが気になったぐらいです.

どうやら、こちら Windows SDK for Windows 7.1 をインストールするとエラーが発生する - Windows - Project Group の 現象だったようです. “A problem occurred…” なんて表示されなかったようにも思いますが、色が付いているわけでもないので…
上記サイトの情報をもとに、Microsoft Visual C++ 2010 x64/x86 Redistributable を 確認したところ、見事に x64 の方が入っていました. アンインストールしてから再度 SDK を 入れたところ、無事にインストールができました. こちらのサイトの情報がなかったら完全にアウトだったかもしれません. 助かりました. 有益な情報ありがとうございます!


いろいろと難所はありましたが、無事に OpenSSL を Windows に 入れることができました.
Windows 10 の Bash on Ubuntu on Windows なら、こんな苦労をしなくても良いのかなぁと思いつつも、まだまだ Windows 7 を 使うのでした…

Slack の ボット で 今日の天気を通知する - JSON 取得編

ボットを使って、Slack に 今日の天気を通知してもらうようにしたいと思います.
天気関連はいろいろなサービスがあり、それを使うのも手なのですが自分でカスタマイズした細かいところにこだわったツールにしたいと思うので、ボットに教えてもらうことにします.

作業環境

  • Node.js 6.9.1 LTS

天気の情報を提供してくれるサービス

今回は OpenWeatherMap の API を 使って情報を取得したいと思います.
Openweathermap - Wikipedia によると、OpenWeatherMap は 各種気象データを 無料 で API 提供してくれるサービスとのことです. しかも全データは、CC BY-SA に 準じるとのことで、情報の改変や商用利用が許可されています. ありがたい!
※ OpenWeatherMap の ToS は こちら Terms of Service

無料で利用できますが、主な利用条件は以下となります.

  • 1分間 の API 呼び出し回数は 60回
  • 現在の天気 に 加え、3時間ごと 5日間の予報が使える
  • 天気情報の更新 は 2時間以内
    ※ 情報の更新について、Wikipedia に 10分以内となってますが、10分以内は Pro 以上で、Free は 2時間以内となっている

参考情報
天気情報が提供されているサービスについては、こちらのサイトを参考にさせて頂きました. 素晴らしい情報 ありがとうございます!

API キー の 取得

OpenWeatherMap の Sing Up サイト https://home.openweathermap.org/users/sign_up へ アクセスします.
全ての項目を入力、ToS/PP を 確認し同意したら I agree ~~~ に チェックして、[Create Account] ボタンをクリックします.

利用目的を聞かれるので、法人利用の場合は [Company] も 入力し、[Purpose] を 選択します.

無事、API キー が 発行されました.
Welcome メール は 届きますが URL クリックによる認証などがなく、簡単にサインアップさせてくれるので助かります.

現在の天気情報を取得する

Botkit に 組み込むので、Node.js で コーディングします.
※ [API_KEY] を 上記で取得した API キー に 置き換えます.

1
2
3
4
5
6
7
8
9
const http = require('http');
http.get("http://api.openweathermap.org/data/2.5/weather?id=1850147&units=metric&appid=[API_KEY]", (response) => {
let buffer = '';
response.setEncoding('utf8').on('data', (chunk) => { buffer += chunk; });
response.on('end', () => {
let current = JSON.parse(buffer);
console.log(current)
});
});

HTTP GET で 取得して、JSON 出力をする簡単なものになります. 特別な実装部分はありません.
URL の パラメーターは以下となります.

  • id=1850147 は、天気情報を取得する 都市 の ID (今回は東京、ID の 取得は後述)
  • units=metric は、摂氏華氏(°C/°F) を気温を摂氏にするために指定 (デフォルトは華氏)
  • appid=[API_KEY] で、API キーを指定

実行すると以下のような JSON が 出力されます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{ coord: { lon: 139.69, lat: 35.69 },
weather:
[ { id: 801,
main: 'Clouds',
description: 'few clouds',
icon: '02d' } ],
base: 'stations',
main:
{ temp: 6.85,
pressure: 1019.67,
humidity: 80,
temp_min: 6.85,
temp_max: 6.85,
sea_level: 1023.45,
grnd_level: 1019.67 },
wind: { speed: 2.96, deg: 2.5058 },
clouds: { all: 12 },
dt: 1485145194,
sys:
{ message: 0.0063,
country: 'JP',
sunrise: 1485121634,
sunset: 1485158355 },
id: 1850147,
name: 'Tokyo',
cod: 200 }

天気は、weather[0].main に なります. なぜ weather が 配列なのかはちょっとわかりませんが…
なお、今回の例では Clouds なので曇りです.
JSON の 内容については、次回にしたいと思います.
※ OpenWeatherMap の 現在の天気 API 仕様 は こちら、Current weather data

3時間ごと 5日間 の 天気予報を取得

先の実装と変わるのは URL の パス が /weather から /forecast に なる点です.

1
2
3
4
5
6
7
8
9
const http = require('http');
http.get("http://api.openweathermap.org/data/2.5/forecast?id=1850147&units=metric&appid=[API_KEY]", (response) => {
let buffer = '';
response.setEncoding('utf8').on('data', (chunk) => { buffer += chunk; });
response.on('end', () => {
let forecast = JSON.parse(buffer);
console.log(forecast)
});
});

実行すると以下のような JSON が 出力されます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{ city:
{ id: 1850147,
name: 'Tokyo',
coord: { lon: 139.691711, lat: 35.689499 },
country: 'JP',
population: 0,
sys: { population: 0 } },
cod: '200',
message: 0.0147,
cnt: 38,
list:
[ { dt: 1485151200,
main:
{ temp: 6.12,
temp_min: 6.12,
temp_max: 6.12,
pressure: 1022.42,
sea_level: 1023.53,
grnd_level: 1022.42,
humidity: 59,
temp_kf: 0 },
weather:
[ { id: 802,
main: 'Clouds',
description: 'scattered clouds',
icon: '03d' } ],
clouds: { all: 36 },
wind: { speed: 7.62, deg: 319.001 },
sys: { pod: 'd' },
dt_txt: '2017-01-23 06:00:00' },
{ ... (3時間ごとの繰り返し) }
]
}

天気は、予報 38回分なのでリスト list に 入っています. その中の構造は現在の天気とほぼ同じです.
直近の予報を取得するには list[0].weather[0].main に なります. 今回の例では Clouds なので曇りです.
※ OpenWeatherMap の 天気予報 API 仕様 は こちら、5 day weather forecast

都市 ID の 取得方法 と 他の検索方法 について

OpenWeatherMap の 天気情報および予報について、取得する都市や地域の指定は以下の 4種類で指定できます.

指定方法  クエリ例 概要
都市名  q=tokyo,jp 都道府県以下は曖昧になり意図とは異なる場合も.
都市 ID  id=1850147 OpenWeatherMap の ID 指定. 確実!
地理座標  lat=35.69&lon=139.69 明確な指定方法と思われるが緯度経度を調べるのが大変…
郵便番号  zip=1000005,jp これも明確な指定方法と思われる

いろいろな方法がありますが当然のことながら、すべての都市や緯度経度に天気観測のステーションがあるわけではなく、近隣のステーション情報へのマッピングがされていると考えると、都市 ID が 一番確実だと考え、今回は 都市 ID で 指定するようにしました.

また、公式にも “We recommend to call API by city ID to get unambiguous result for your city. – OpenWeatherMap“ と 謳われているので、都市 ID で 指定したほうがよいのでしょう.

都市 ID は http://bulk.openweathermap.org/sample/ から取得します.

city.list.json.gz が 都市情報のデータになります.
全世界の都市データが入っており、2017年1月現在で 全 209,578 件 で、"country":"JP" でも 1,402件です. すごい.
そして、なぜか、御影 芦屋 松山市 は 漢字で都市名が入っている. なぜだろう…

以下に 日本 と 東京、御影芦屋松山市 を 抜粋し、Google マップ による緯度経度検索結果の場所を載せておきます.
_id の 値が 都市 ID になります. (実際はスペースなし、投稿用に整形済み)

1
2
3
4
5
6
7
8
9
{ "_id": 1861060, "name": "Japan",         "country": "JP", "coord": { "lon": 139.753098, "lat": 35.68536  }}  // 皇居
{ "_id": 1850147, "name": "Tokyo", "country": "JP", "coord": { "lon": 139.691711, "lat": 35.689499 }} // 東京都庁
{ "_id": 1850144, "name": "Tōkyō-to", "country": "JP", "coord": { "lon": 139.691711, "lat": 35.689499 }} // 東京都庁
{ "_id": 1864529, "name": "Chiyoda-ku", "country": "JP", "coord": { "lon": 139.753632, "lat": 35.694019 }} // 千代田区役所
{ "_id": 7302982, "name": "御影", "country": "JP", "coord": { "lon": 135.252426, "lat": 34.724258 }} // 阪急 御影駅?
{ "_id": 7302983, "name": "芦屋", "country": "JP", "coord": { "lon": 135.30719, "lat": 34.733921 }} // JR 芦屋駅?
{ "_id": 1864985, "name": "Ashiya", "country": "JP", "coord": { "lon": 135.302643, "lat": 34.728069 }} // 阪急 芦屋駅?
{ "_id": 7303001, "name": "松山市", "country": "JP", "coord": { "lon": 132.756729, "lat": 33.83905 }} // 松山市内?
{ "_id": 1926099, "name": "Matsuyama-shi", "country": "JP", "coord": { "lon": 132.765747, "lat": 33.839161 }} // 松山市役所



簡単に利用できる API を 無料で公開してくれるサービスのおかげで、容易に天気情報が取得できました. 毎朝の天気を Slack に 流すだけなら、もう Botkit に 組み込めそうですが、取得できたデータをしっかり把握したいので、次回は JSON の 内容について確認したいと思います.
Slack の ボットで定時アクション が できるようになったところで、定番の天気予報に入らず Slack の ボット で JRA 競馬 の 開催日を通知 と いきなり脱線しましたが、軌道修正して基本の天気予報を流せるようにしていきたいと思います.

Slack の ボット で JRA 競馬 の 開催日を通知する - Slack ボット 実装編

前回、JRA の サイト から 開催日 iCalendar を 取得し、ついに Slack の ボットへ組み込む準備ができました! ボットへ組み込み、開催日を教えてもらいましょう.

作業環境

  • Slack
  • Node.js 6.9.1 LTS
  • Botkit 0.4.9
  • unzip 0.1.11
  • ical2json 1.0.0
  • node-cron 1.2.1
  • moment-timezone 0.5.10

通知方法の検討

今回は GI レースに限定し、開催日 10日前 の 午前10時 に 通知するようにしたいと思います. Slack の チャンネルは、とりあえず実験用の #sandbox へ ポストするようにして様子見します.
土日などの連続開催がある場合、開催日 10日前 だと連続して通知が来るようになってしまいますが、GI レースに限るとあまりないのでよしとします. GII や GIII が 入ってくると、土日連続がかなりあるようなので本格的に通知する場合は、同時通知できるようにした方がよさそうです.

Slack ボット の 実装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
'use strict';
const Botkit = require('botkit');
const http = require('http');
const cron = require('cron');
const unzip = require('unzip');
const moment = require('moment-timezone');
const ical2json = require('ical2json');

const controller = Botkit.slackbot();
controller.spawn({
token: process.env.bot_access_token
}).startRTM((err, bot, payload) => {
moment.locale('ja');
moment.tz.setDefault('Asia/Tokyo');

http.get(`http://www.jra.go.jp/keiba/common/calendar/jrarace2017.zip`, (response) => {
let buffer = '';
response.pipe(unzip.Parse()).on('entry', (entry) => {
entry.on('data', (chunk) => { buffer += chunk; });
entry.on('end', () => {
let ical = ical2json.convert(buffer)['VEVENT'];
for (let i in ical) {
let data = ical[i];
if (data['SUMMARY'].includes('GII')) { continue; }
new cron.CronJob({
cronTime: moment(`${data['DTSTART;VALUE=DATE']}T1000+0900`).subtract(10, 'days').toDate(),
onTick: () => {
let date = moment(this.race['DTSTART;VALUE=DATE']).format('M/D(ddd)');
let race = this.race['SUMMARY'].substring(0, this.race['SUMMARY'].indexOf('('))
bot.say({
text: `${date}${race}${this.race['LOCATION']} だよ`,
channel: 'sandbox'
});
},
start: true,
timeZone: 'Asia/Tokyo',
context: { race: data }
});
}
});
});
});
});

Slack ボット の 基本的な作りは Slack の ボット で 定時アクション の 実装と変わりがありません. また、iCalendar の 取得部分も Slack の ボット で JRA 競馬 の 開催日を通知する - iCalendar 取得編 と なります. これらの組み合わせでできています.

cron.CronJob が HTTP GET の後に、繰り返しで作られているところがポイントになります.
開催日の情報を moment-timezone モジュールでパースし通知したい時刻とタイムゾーンを付けます. そして subtract(10, 'days') で 10日前の日付にし、cronTimeDate オブジェクトとして渡します. node-cron は crontab の 文字列だけでなく Date オブジェクトも渡すことができるので、このように特定日の10日前といった指定ができます.

あとは onTick で ボットに話してもらうだけなのですが、ボットが話す際に開催情報が必要となります. これは new cron.CronJob() する際に、context で ジョブが起動した際に渡したいオブジェクトを指定することで可能となります. 今回は context: { race: data } とすることで iCalendar の データ data オブジェクト を race という名前でコンテキストに登録しておきました.
onTick の 際には、this.race と コンテキストに渡した名前に this を つけてアクセスします.

※ 今回は #sandbox へ 発言するので channel: 'sandbox' としていますが、ID で 指定したほうがよいので https://slack.com/api/channels.list?token=[API_TOKEN] へ アクセスして、ID を 調べます. また、発言先のチャンネルへボットが参加している必要があります.

通知!


今年最初の GI レースは 2月19日(日曜日) フェブラリーステークス @東京競馬場 が 通知されました!
ちょっと先なので、実験用に cronTime の 指定をいじりました. T1000+09001000 でなく動作検証をする時間にし、
subtract(10, 'days') を 2月19日 から 動作検証する日まで引いてあげます.
本投稿を書いているタイミング 1月21日 23時ごろで考えると T2300+0900subtract(29, 'days') に なります.



ボットで JRA の GI レース開催日 を Slack に 通知することができました. これで予め準備の上で観戦に臨めそうで楽しみです. (その前に基本的なことを勉強しておかないと…)

定時アクションができるようになり 定番の天気予報に着手しようと思っていましたが、思わぬ方向の機能を作ることになりましたが、おもしろい物ができたと思います. 思いついた時こそ勉強のチャンスですね.

Slack の ボット で JRA 競馬 の 開催日を通知する - iCalendar 取得編

前回、JRA の サイト から 開催日 JSON を 取得しました. ただ この方法については公開されているものを使っておらず、ウェブサイトのためのデータを勝手に使っているので お行儀が悪いです.
適切なデータを使って開催日の情報を扱えるようにしたいと思います.

作業環境

  • Node.js 6.9.1 LTS
  • unzip 0.1.11
  • ical2json 1.0.0

開催日情報の取得方法について「再」検討

データを取得する方法について、もう少し検討を進めたいと思います.
重賞レース一覧 の ページがあり、こちらは HTML に 開催日情報 が 記述されています. スクレイピングすることでデータ化することができそうですが、HTML の 構造が変わるなどすると使えなくなってしまうので、最終手段にとっておきたいところです.

よくよくウェブサイトを見ていくと、レーシングカレンダー・ページ の 末尾 に 他のカレンダー と 連携 の リンク が ありました!

この 他のカレンダー と 連携 ページを確認すると、iCalendar 形式のファイルをダウンロードすることができるとのことです.

これなら公開されているデータなので、安心して使うことができます. 年間開催のスケジュール と 年間の重賞スケジュール の 二つが用意されているようで、今回は重賞スケジュールのファイルを取得することにします.
また “開催中止等に伴うスケジュールの変更には対応しておりません。予めご了承ください。 – JRA” とのことですが、今回は開催日を通知することが目的で厳密なスケジュール運用は必要としないので、よしとします.

iCalendar の 取得方法

前回と変わらず http モジュールで取得したいと思います.
今回は iCalendar ファイル が Zip で アーカイブされているので、その取扱いが必要となります.
Zip の 解凍は HTTP の レスポンスを Stream で 流し込めるので unzip モジュールを使わせてもらいました. また iCalendar の 扱いは JSON に してほしかったので ical2json モジュールを使わせてもらいます. それぞれ標準では入っていないので npm install [module name] --save で インストールしておく必要があります.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const http = require('http');
const unzip = require('unzip');
const ical2json = require('ical2json');

http.get(`http://www.jra.go.jp/keiba/common/calendar/jrarace2017.zip`, (response) => {
let buffer = '';
response.pipe(unzip.Parse()).on('entry', (entry) => {
entry.on('data', (chunk) => { buffer += chunk; });
entry.on('end', () => {
let ical = ical2json.convert(buffer)['VEVENT'];
for (let i in ical) {
let data = ical[i];
console.log(`${data['DTSTART;VALUE=DATE']} ${data['SUMMARY']}`);
}
});
});
});

構造としては、前回 の JSON 取得と変わりありません.
response を 直接扱うのではなく unzipParse() に パイプして、HTTP GET で 取得するデータを直接 unzip に 流すところがポイントになります.
これにより 一時ファイルに落とさず直接解凍して処理を行うことができます. 今回も解凍して 45 KB ちょっとなので、変数に入れてメモリ上で処理してしまいます.
あとは ical2json モジュールが iCalendar の フィールド名 を キーとして JSON に してくれるので、プログラムで簡単に扱うことができます.



無事に公開されているデータで開催日を取得することができました. JSON のように データが構造化しきれないので、グレード は 自前で文字列処理が必要となりますが、十分利用できそうです. なにより 1年分が 1回で取れるのがよいですね.
ファイル置き場の URL が 変わらなければ、年数の部分だけ自動処理すれば使いづづけられそうです. 年単位なので いきなり変わることもありそうだけど…

Node.js には いろいろなモジュールがあるので選択に迷いますが、素晴らしいモジュールを提供してくださっている方々のおかげで簡単に実装できるので助かります. ありがとうございます!

Slack の ボット で JRA 競馬 の 開催日を通知する - JSON 取得編

2016年 の 有馬記念 を 見て、急に競馬が気になりだしました. とはいえ、ちゃんとやったこともないし、いつやっているのかも知らない、といったレベルなので、次週の開催予定をボットに通知してもらうようにして、下調べぐらいはしてから見れるようにしたいと思います.
ボットにしゃべってもらうにはデータが必要なので、まずは開催日のデータ探しと取得方法について考えます.
→ 公開されているデータを取得するよう Slack の ボット で JRA 競馬 の 開催日を通知する - iCalendar 取得編 の 記事を追加しました. (2017年1月18日追記)

作業環境

  • Node.js 6.9.1 LTS

開催日情報の取得方法について検討

JRA の サイトに レーシングカレンダー があり、さまざまな情報が載っています. しかし開催情報を取得できるような Web API は なさそうです. また、そのような情報を提供しているサイトも見当たりませんでした. (なんか、ありそうな気もするんですが…)

レーシングカレンダー の ソースを見ると、JavaScript で カレンダー生成を行っていることが分かります. ソースには 2015 と ありますが、2017年も順調に機能しているようです.

その中に JSON ファイルを取得している部分があります! この JSON ファイル を 使う手もありそうです.

過去の JSON ファイルを探っていくと、2012年1月 が 一番古いようです. カレンダーの描画部分は 2015年にリニューアルしたとして、システム自体は 2012年には稼働していたのでしょうか.

JSON の 取得方法

とりあえず、この JSON を 取得してみたいと思います.
Node.js で HTTP リクエストするには、http モジュールを使います. 標準で組み込まれているので簡単に使い始められます. もう少し手軽に扱ったり、複雑なことをするには request が よいようですが、今回は簡単な HTTP GET なので http で いきたいとおもいます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let http = require('http');
http.get(`http://www.jra.go.jp/keiba/common/calendar/json/201701.json`, (response) => {
let buffer = '';
response.setEncoding('utf8').on('data', (chunk) => { buffer += chunk; });
response.on('end', () => {
let json = JSON.parse(buffer)[0].data;
for (i in json) {
for (j in json[i].info[0].gradeRace) {
let data = json[i];
let race = data.info[0].gradeRace[j];
console.log(`${data.date}(${data.day}) ${race.grade} ${race.name}`);
}
}
});
});

一時ファイルに落としても良かったのですが、ファイルとして取っておく必要もなく 1 JSON ファイルあたり 7 KB ちょっとなので変数で直接持ってしまいます.
httpdata イベントで読み込んだ分の情報を返します. 全てを読み込んだ結果ではないことに注意です. 読み込みが終わるまで変数に追加し保持しておきます.
全てが読み込み終わると end イベントが発生するので、このタイミングで保持しておいたデータを処理します. 今回は読み込んだ情報を JSON オブジェクトにし、1行ごとに開催日とグレード、レース名をコンソールに出すようにしました.

これを 月ごとの JSON ファイルを取得するようにすれば、開催日の情報をボットに通知してもらうことができそうです.



JRA の 開催日情報を取得できました. これを活用することで事前にレース開催日を知ることができるので、心して観戦に望めそう
ところで この JSON、JRA の ウェブサイトのカレンダーを描画するためのデータをもらっているわけですが、公式に公開されているものではないので勝手に使うのは お行儀がよくないです. やはりちゃんと公開されているデータを使いたいところです.

Slack の ボット で 定時アクション

Slack に 設置したボットを一定時間ごとに発言するようにしたいと思います.
定時アクションができるようになると、天気の情報を毎朝ポストしてもらうなど活用の幅が広がります.

作業環境

  • Slack
  • Node.js 6.9.1 LTS
  • Botkit 0.4.9
  • node-cron 1.2.1

Node.js に cron を 追加

Linux などの OS で 定期的な処理を行うには cron → crontab - Wikipedia が 使われます. Node.js でも同様に cron と呼ばれるモジュールがいくつかあります.
今回は Kelektivさん の node-cron を 使いたいと思います.

ボットのプロジェクト・ディレクトリで以下のコマンドを実行し node-cron モジュールをインストールします.

1
2
c:\Develop\repos\slack-bot> npm install cron --save
`-- cron@1.2.1

メモ
node-cron という名前のモジュールは Kelektivさん と Merenciaさん の 2つがあるので注意が必要です.
今回は タイムゾーンが指定できることと 、日付指定の実行ができることなどから Kelektivさん のを使わせていただきました.
特にタイムゾーンを意識する必要がない場合は、Merenciaさん のも シンプルな実装でよいかもしれません.

Botkit での 定時処理

とりあえず、月曜日~金曜日 の 毎朝 9:00 に 挨拶をするようにしたいと思います.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const Botkit = require('botkit');
const cron = require('cron');

const controller = Botkit.slackbot();

controller.spawn({
token : process.env.token
}).startRTM((err, bot, payload) => {
new cron.CronJob({
cronTime: '00 00 09 * * 1-5',
onTick: () => {
bot.say({
channel: 'random',
text: ':smiley: おはようございます!'
});
},
start: true,
timeZone: 'Asia/Tokyo'
});
});

  • ボット起動時に cron の 処理を登録したいので、startRTM() の 中で new cron.CronJob() します.
  • cronTimeCron Ranges に従い、起動したい時間を指定します. 今回は「月曜日~金曜日 の 毎朝 9:00」なので、前半 の 00 00 09 ~~~ で 9:00 を 指定し、後半 の ~~~ * * 1-5 で 毎月毎日(* * ~~~) に 加えて 最後の 1-5 で 月曜日~金曜日(0 は 日曜日) を 指定します.
  • onTickcronTime で 指定したタイミングで行う処理を記述します. 今回は Botkit に #random チャンネル へ 挨拶を発言しさせます.
  • starttrue にすることで、そのまま cron の 処理を開始させます. 処理開始を明示的に行いたい場合は false にし、開始したい場所で start() を 呼び出しますが、今回は即時に処理を開始してよいので true にしました.
  • timeZonecronTime に 指定するタイムゾーンを明示します. 最近はクラウド環境を使ったりするのでタイムゾーンは意識的に書いておいた方がトラブルに合いにくい気がします.

※ 今回は #random へ 発言するので channel: 'random' としていますが、ID で 指定したほうがよいので https://slack.com/api/channels.list?token=[API_TOKEN] へ アクセスして、ID を 調べます. また、発言先のチャンネルへボットが参加している必要があります.

実行!

通常通り起動し、cronTime で 指定した時間を待ちます. 無事、ボットが挨拶をしてくれたでしょうか!?



Node.js の モジュールを使うことで簡単に、ボットの定時アクションを実装することができました! モジュールを作ってくださる方々のおかげで、やりたいことがすぐにできるので助かります. 感謝感謝です.
次回はオーソドックスに天気を知らせてくれる機能を追加しようかな~

Hexo の 投稿記事 URL を 変更する

Hexo の 投稿記事 の URL は、タイトルが使われます. その際に半角スペースをハイフン(-) に 置き換えてくれるのですが、私は日本語とアルファベットが混ざる際に半角スペースを入れてレイアウトを調整します. そうなると URL 日本語なのにハイフンだらけとなってしまいます. 昔のブラウザは URL エンコードされた文字列だったので気にもならなかったのですが、最近のブラウザはでコードして日本語で表示してくれるので気になってしまうので、極力 ハイフンなし の URL に 戻したいと思います.

作業環境

  • Windows 7
  • Hexo 3.2
  • hexo-generator-alias 0.1.3

記事のファイル名を変更する

URL を 変更するには、各投稿記事のファイル名を変更し再生成するだけです. 簡単!
アルファベットの区切りとしての半角スペース(=ハイフン) は よいので、日本語とアルファベットの間でレイアウト調整した分だけ、ハイフンを消してファイル名変更します.

メモ
ファイル名を変更するということは、古いファイルを消して、新しいファイルを作ることなのですが、hexo generate は 生成済みのファイルの削除は行ってくれないようです. 基本的に投稿を削除するということはないので通常はそれでよいのですが、今回のようにファイル名変更した場合は hexo clean して生成済みのファイルを削除してから hexo generate する必要があります.

古い URL からのリダイレクトを設定

ファイル名を変更することで URL を 変更できましたが、古い URL へのケアが必要です. リンクしてくださっている サイト や SNS の 投稿などがありましたら 404 Not Found に なってしまいますし、Google などの 検索エンジンからは重複記事と見えてしまいます. そのようなことにならないよう、古い URL に リダイレクトの設定をしておきます.

Hexo で リダイレクト設定するには、hexo-generator-alias を 使います.
Hexo の ソースがあるフォルダで npm install hexo-generator-alias --save を実行します. ついでに各 Plugin の アップデートもしておきます. (下記例の [username] は 自分の GitHub ユーザ名)

1
2
3
4
5
C:\Develop\repos\[username].github.io> npm update
C:\Develop\repos\[username].github.io> npm install hexo-generator-alias --save
`-- hexo-generator-alias@0.1.3
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@^1.0.0 (node_modules\chokidar\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.0.17: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

リダイレクトの設定 は 各投稿のファイル もしくは、_config.yml で 行います.
_config.yml は 全体設定を置いておきたいので、今回は各投稿のファイルに書きます. たとえば本ブログの最初の記事の例で、以下のように Front-matter へ alias: [古い URL] を 追加します.

1
2
3
4
5
6
7
8
---
title: Hexo GitHub Pages ブログ環境の構築
date: 2016-11-01
categories: ブログ
tags:
- Hexo
alias: /2016/11/01/Hexo-と-GitHub-Pages-で-ブログ環境の構築/
---

対象となる投稿記事の設定が終わったら、hexo clean, hexo generate で 再生成します.
これにより公開フォルダが以下のように生成され、古い URL にも index.html が 生成されています.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c:\Develop\repos\personal\[username].github.io>tree /F
C:.
├─.deploy_git
├─.settings
├─node_modules
├─public
│ ├─2016
│ │ ├─11
│ │ │ ├─01
│ │ │ │ ├─Hexo-と-GitHub-Pages-で-ブログ環境の構築
│ │ │ │ │ index.html
│ │ │ │ └─HexoとGitHub-Pagesでブログ環境の構築
│ │ │ │ index.html

├─scaffolds
├─source
└─themes

古い URL の index.html は、以下のようにリダイレクトのソースが生成されています. (実際は 1行、投稿用に整形済み)

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Redirecting...</title>
<link rel="canonical" href="/2016/11/01/HexoとGitHub-Pagesでブログ環境の構築/">
<meta http-equiv="refresh" content="0; url=/2016/11/01/HexoとGitHub-Pagesでブログ環境の構築/">
</head>
</html>


URL の 変更を無事に行うことができました. 最初からちゃんと URL 構成を考えておけば、こんなことにならなかったわけですが、まぁ仕方ない. エイリアス設定の勉強ができたのでよしとしましょう.

ところで本件の原因となった、全角と半角が混ざる際に半角スペースを入れたくなる問題、本当は CSS なりのスタイル側で処理してくれるとよいもので、文章上の意味を持たないのにレイアウトとして半角スペースを入れるのはよくないのかなぁとは考えています…
とはいえ現状はスタイル側で対応できないので致し方なし、といういうのと対比となる文の場合で強調するほどでもないときに半角スペースを入れることで、自然な強調にできるのでよく使っていたりもします.

このあたり、みなさん いろいろと苦労されていらっしゃるようで、私もよい解があったら ぜひ乗りたいなぁ.

Hexo で PubSubHubbub 通知をする

Hexo の フィード出力を設定した際に、設定項目で気になったものがありました. “hub - URL of the PubSubHubbub hubs (Leave it empty if you don’t use it) – hexo-generator-feed“ です.

そもそも PubSubHubbub 自体を知らなかったのですが、”パブサブハバブ – Wikipedia“ と 読むそうで、データの変更(ここでは、ブログの更新情報) を リアルタイムに通知するためのプロトコルなのだそうです.

この仕組みを使うことで、Google などの検索エンジンにブログの更新を効率よく通知することができ、インデックス化を早めることができるとのことで、早速導入したいと思います.

作業環境

  • Windows 7
  • Hexo 3.2
  • hexo-generator-feed 1.2.0
  • GitHub (GitHub Pages / Webhook)

PubSubHubbub とは?

PubSubHubbub は、分散型 の パブリッシュ(発行)/サブスクライブ(購読) コミュニケーション を 行うためのオープンなプロトコルで、仕様は https://github.com/pubsubhubbub/PubSubHubbub で 公開されています. 2017年1月現在のバージョン は 0.4 です.

ざっくり言うと、情報の提供者と利用者の間にハブを置くことで、それぞれが分離した作業ができるようにし、またプッシュ型にすることで情報の利用者が提供者のサーバへ定期的なポーリングせず不要な負荷を下げることができるようになります.
利用例として気象庁の電文公開があります.

今回は、この仕組みを使い Google PubSubHubbub Hub へ ブログの更新情報をプッシュし、Google の クローラー へ ブログの更新に関する情報を購読してもらいます. これによって Google の クローラー が 定期的にブログの更新を確認しに来てくれていたのを、こちらからクローラーへ来てもらうことができるようになります.

これまでは Google の クローラーが来てくれるのを ただ待っていただけですが、PubSubHubbub を 使うことでクローラーを呼び込むことができ、検索エンジンのインデックス化を格段に早めることができるようになります.

Hexo に PubSubHubbub を 設定

PubSubHubbub は RSS や Atom フィード で 情報を提供します. hexo-generator-feed が PubSubHubbub の 情報生成に対応しているので、まずは hexo-generator-feed の 設定を行います.

config.yml の hexo-generator-feed に 関する設定に hub: http://pubsubhubbub.appspot.com を 追加します.

1
2
3
4
5
feed:
type: atom
path: atom.xml
limit: 20
hub: http://pubsubhubbub.appspot.com

hexo generate で サイトを再生成してローカルサーバ http://localhost:4000/atom.xml でフィードを確認してみると、<link href="http://pubsubhubbub.appspot.com" rel="hub"/> が 増えているのが分かります.

GitHub の Webhook で PubSubHubbub 通知設定

hexo-generator-feed の 設定はフィードの出力設定で、PubSubHubbub の ハブ (Google PubSubHubbub Hub) への通知は別途行う必要があります.
GitHub Pages で ウェブサイトを公開しているので、今回は GitHub の Webhook を 使って更新時にハブへ通知するようにしたいと思います.

GitHub リポジトリ の Setting ページ から、Webhooks を 表示し、右側の [Add webhook] ボタンをクリックします.

以下の情報を入力し、[Add webhook] ボタンをクリックします.

設定項目 設定内容
Payload URL [PubSubHubbub 通知 URL(※)]
Content type application/x-www-form-urlencoded を 選択
Secret [空欄]
Which events… Let me select individual events.Page build に チェック

[PubSubHubbub 通知 URL] は https://pubsubhubbub.appspot.com/publish?hub.mode=publish&hub.url=https://[username].github.io/atom.xml で、[username] を 自分のユーザ名に置き換える、もしくは hub.url= 以降に自分のサイトのフィード の URL にします.
心配な場合は https://pubsubhubbub.appspot.com/publish で 正しいか検証することができます.

また [Which events…] は Page build だけにし、GitHub Pages の ページがビルドされた時だけ通知するようにします. Push だと、ドラフトの記事を GitHub に 上げた際にも通知されてしまい、不要な通知がハブへ行ってしまいます.

Webhook が 追加されました. タイミングによっては通知のテストが行われておらずチェックがついてません. URL 部分をクリックし設定から通知テストを確認するかリロードして確認します.

通知テストの結果は Wehbook の 設定画面の一番下にあり、Response が 204 に なっていれば成功です.
実動作の確認としては CircleCI から再ビルドするか、hexo deploy で サイトを更新します. 正しくデプロイできると、この画面 の Recent Deliveries が増えます.


ブログ に PubSubHubbub の 通知を追加できました. これにより、より早く記事が検索できるようになるといいですね. また読んでいただけるような、しっかりした記事をかけるようにしていきたいと思います. どうぞ 今後ともよろしくお願いいたします.

Slack の ボットによる代理ポストで、簡易匿名化

ようやく常時稼働するボットを Slack に 常駐できるようになりました. 今回は Slack で 匿名発言する方法について考えたいと思います.

作業環境

  • Slack
  • Node.js 6.9.1 LTS
  • Botkit 0.4.2

Slack で 匿名発言

そもそもチャットなのに、なんで匿名で発言する必要があるのか、といった話もありますが、チームや組織の改善などを忌憚なくディスカスするには匿名はある程度意味があるようです.
行き過ぎると匿名のネット掲示板のように荒れるというケースもあるようですが、Slack は 招待性のチームで作られるので、ある程度は自制・自浄されることは期待できそうです.

Slack で 匿名発言する方法ですが、公式でその機能はありません. したがって何らかの仕掛けを作る必要があります.
API を 使って発言する方法なども考えられますが、せっかくボットを常駐させたので代理で発言してもらって、匿名化してみたいと思います.

ボット で 代理発言

Slack の ボットは、以前に設置したボット を 使うことにします.
ボットへの代理発言依頼は、ダイレクト・メッセージを使うことにします.

匿名で発言する先のチャンネルを選択(or 作成) します. 今回は sandbox に しました.
続いて チャンネル の ID を 調べるために、以下の URL へ アクセスします. [API_TOKEN] は ボットの起動に使用している API トークンです.
https://slack.com/api/channels.list?token=[API_TOKEN]

ページへアクセスすると JSON 形式 の チャンネル・リストが表示されるので、目的のチャンネルの ID を コピーします. 以下に今回使用する sandbox の 部分を抜粋します. "id": "C3ADKXXXX",C3ADKXXXX に 当たる部分になります.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"ok": true,
"channels": [{
"id": "C3ADKXXXX",
"name": "sandbox",
"is_channel": true,
"created": 1481422490,
"creator": "U3A3DXXXX",
"is_archived": false,
"is_general": false,
"is_member": false,
"members": [ "U3A3DXXXX", U3AQBXXXX ],
"topic": { "value": "", "creator": "", "last_set": 0 },
"purpose": { "value": "", "creator": "", "last_set": 0 },
"num_members": 2
}]
}

ボットの代理発言を実装します.
以下のコードをボットの実装に追加します. [CHANNEL_ID] は 上記で取得した発言先チャンネル の ID です.

1
2
3
4
5
6
controller.on('direct_message', (bot, message) => {
bot.reply(message, '匿名でポストしました.');
bot.startConversation({ channel : '[CHANNEL_ID]' }, (err, convo) => {
convo.say(message);
});
});

実装の内容としては以下となります.

  • controller.on()direct_message イベント に 反応するようにします.
  • bot.reply() で ダイレクト・メッセージに返事をします.
  • bot.startConversation() で ID 指定したチャンネルで会話を開始します.
  • convo.say(message); で 指定チャンネルへ message、ユーザから入力された内容を そのままに指定チャンネルへ発言します.

匿名で発言!

DIRECT MESSAGES の ボット名をクリックし、発言したい内容を入力します. (つまりボットに対してダイレクト・メッセージを送ります)

ダイレクト・メッセージを送るとボットから返事があり、すぐに匿名発言先のチャンネル (ここでは sandbox) に 発言があるハイライトがされます.

チャンネルを開くと、先ほどボットにダイレクト・メッセージした内容を、ボットが発言しています.
匿名発言ができるようになりました.



ボットを使って匿名発言ができるようになりました. Botkit が いろいろやってくれるので簡単ですね.

ただし匿名とはいえ単にボットで代理発言をさせているだけなので、ボット・プログラムでデバッグログなりを出すことで発言を残すことはできますので、完全匿名とまではいかないですが、自由な発言から議論が広がるといいですね.

Slack の ボット を 自動停止させない

2017年3月10日 更新
Botkit 0.5.1 から自動切断されなくなったようです.
本ポストのコードは不要ですが、記録のために残しておきます.

Slack に 設置したボット. しばらくするといつの間にかオフラインになっている場合があります…
せっかく作ったボット機能をいつでも簡単に使うというわけにいかないので対策を考えたいと思います.

作業環境

  • Slack
  • Node.js 6.9.1 LTS
  • Botkit 0.4.2

いつの間にかオフライン?

設置したボットに翌日話しかけてみたところ、応答がありませんでした. 残念…
よく見るとオフラインになっています.

ログを確認したところ notice: RTM close event: 1006 : なる出力があり、その後の再接続に失敗しているようです.
起動時と停止時に時間を出力するようにしたところ、約8時間ぐらいで止まっている感じでした. (もしかしたら途中でボットを呼んだから、なんとなく切りよくと考えると6時間?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c:\Develop\repos\slack-bot> set token=[API_TOKEN]
c:\Develop\repos\slack-bot> node index.js
Mon Dec 19 2016 15:49:08 GMT+0900 (東京 (標準時))
info: ** No persistent storage method specified! Data may be lost when process shuts down.
info: ** Setting up custom handlers for processing Slack messages
info: ** API CALL: https://slack.com/api/rtm.start
notice: ** BOT ID: bot ...attempting to connect to RTM!
notice: RTM websocket opened
info: ** API CALL: https://slack.com/api/chat.postMessage
notice: RTM close event: 1006 :
Mon Dec 20 2016 07:29:10 GMT+0900 (東京 (標準時))
error: Abnormal websocket close event, attempting to reconnect
notice: ** BOT ID: bot ...reconnect attempt #1 of 3 being made after 1000ms
info: ** API CALL: https://slack.com/api/rtm.start
notice: ** BOT ID: bot ...reconnect attempt #2 of 3 being made after 6297ms
info: ** API CALL: https://slack.com/api/rtm.start
notice: ** BOT ID: bot ...reconnect attempt #3 of 3 being made after 9096ms
info: ** API CALL: https://slack.com/api/rtm.start
error: ** BOT ID: bot ...reconnect failed after #4 attempts and 9096ms

何が起こった?

ログに RTM close event と あるように、RTM が クローズされたとのこと. これは日付を出力するために rtm_close イベントをフックしているのでわかっている部分で、では何故イベントが発生したのでしょうか.
rtm_close イベント を 発生させているのは、ログその後のメッセージから Slack_web_api.js の 以下の部分と思われます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bot.rtm.on('close', function(code, message) {
botkit.log.notice('RTM close event: ' + code + ' : ' + message);
if (pingTimeoutId) {
clearTimeout(pingTimeoutId);
}
botkit.trigger('rtm_close', [bot]);

/**
* CLOSE_ABNORMAL error
* wasn't closed explicitly, should attempt to reconnect
*/
if (code === 1006) {
botkit.log.error('Abnormal websocket close event, attempting to reconnect');
reconnect();
}
});

そうなると、今度は bot.rtmclose イベントを発火した部分を確認する必要があります.
この bot.rtmbot.rtm = new Ws(res.url, null, {agent: agent}); で、var Ws = require('ws'); です.
Node.js の WebSocket ライブラリ ws です. ちょっと深くなってきました…
そして、残念ながらクローズの条件となる部分を特定することができませんでした… (技術力足りてない orz)

では、エラー・コード 1006 とは?

6~8時間放置したら止まったのできっとタイムアウトしたのでしょう. と 思い込むことにして、気を取り直しエラー・コードの 1006 について調べたいと思います.

1
2
3
4
if (code === 1006) {
botkit.log.error('Abnormal websocket close event, attempting to reconnect');
reconnect();
}

Abnormal websocket close event と メッセージが出力されている通り、異常終了した感があります.
こちらもコードを追いかけたところ、先の ws の ファイル WebSocket.js に 以下のようなコードがあります.
なんか、デフォルト で 1006 を セットしてメッセージは出さない気がします. ログでも ~: 1006 :1006 の 後にメッセージが付きそうなのに何も出てなかったのは、このことなのでしょう. メッセージをつぶすなんて…

1
2
3
4
5
6
7
// If the connection was closed abnormally (with an error), or if
// the close control frame was not received then the close code
// must default to 1006.
if (error || !this._closeReceived) {
this._closeCode = 1006;
}
this.emit('close', this._closeCode || 1000, this._closeMessage || '');

しかし、この 1006 ハードコードもされ、デフォルトで使われ不思議な感じがします.
この 1006 の 謎をたどると getting the reason why websockets closed - Stack Overflow で 以下のように書かれています.

Close Code 1006 is a special code that means the connection was closed abnormally (locally) by the browser implementation
getting the reason why websockets closed - Stack Overflow

Close Code 1006 だそうで、リンク先を見ると以下のように書かれています.

7.4.1. Defined Status Codes
1006 is a reserved value and MUST NOT be set as a status code in a
Close control frame by an endpoint. It is designated for use in
applications expecting a status code to indicate that the
connection was closed abnormally, e.g., without sending or
receiving a Close control frame.
RFC 6455 - The WebSocket Protocol

ちょっと解りにくいので W3C の WebSocket も あたりました.
“In all of these cases, the the WebSocket connection close code would be 1006”、全部 1006 と 行ってますね… そして上記の RFC 6455 を 見ろと. うーんなるほど.

User agents must not convey any failure information to scripts in a way that would allow a script to distinguish the following situations:

  • A server whose host name could not be resolved.
  • A server to which packets could not successfully be routed.
  • A server that refused the connection on the specified port.
  • A server that failed to correctly perform a TLS handshake (e.g., the server certificate can’t be verified).
  • A server that did not complete the opening handshake (e.g. because it was not a WebSocket server).
  • A WebSocket server that sent a correct opening handshake, but that specified options that caused the client to drop the connection (e.g. the server specified a subprotocol that the client did not offer).
  • A WebSocket server that abruptly closed the connection after successfully completing the opening handshake.
    In all of these cases, the the WebSocket connection close code would be 1006, as required by the WebSocket Protocol specification. [WSP]

Allowing a script to distinguish these cases would allow a script to probe the user’s local network in preparation for an attack.
The WebSocket API

攻撃者から守るために全部 1006 にしてメッセージも出さないということなのだそうで、詳細を知るにはデバッグしていくしかないので、いったん諦めます…

暫定の対策

とりあえず止まらないようにワークアラウンドを設定したいと思います.
Botkit の GitHub Issues Slack RTM retry/reconnect not working #261に 同じような話がありワークアラウンドがありました.
議論の流れからすると reconnect に 関する Pull Request #532 が 上がっておりマージはされているようですが、そのリリースは 0.4.2 には含まれていないので、将来のバージョンアップに期待しつつ、今は @garymoon の コードを 使わせていただきます. ありがとうございます!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const controller = ...;
const bot = ...;

function start_rtm() {
bot.startRTM((err,bot,payload) => {
if (err) {
console.log('Failed to start RTM')
return setTimeout(start_rtm, 60000);
}
console.log("RTM started!");
});
};

controller.on('rtm_close', (bot, err) => {
start_rtm();
});

start_rtm();



これによりノンストップで稼働することができるようになりました. なぜリトライに失敗するのかはデバッグしてかないとわからないので、いずれ確認したいと思いますが、まずはボットに機能を足してボットライフを楽しみましょう♪