プログラム

node-slack-sdkをv4系に上げた際のユーザ及びチャンネル一覧の取得方法

node-slack-sdkのバージョンが3系から4系に変わった際にReal Time Messaging API周りの仕様が一部変わっていて、作っていたSlack眺めるくんのユーザの一覧や自身がjoinしているチャンネルの一覧取得処理を改修した際のメモ。

v3系のとき

v3系だとRTM APIでtokenが認証されたタイミングで発火する処理を記述する

let token = "xxxxxx";
let slack = require("@slack/client");
let RtmClient = slack.RtmClient;
let CLIENT_EVENTS = slack.CLIENT_EVENTS;
let rtm = new RtmClient(token, {logLevel: "error"});
rtm.on(CLIENT_EVENTS.RTM.AUTHENTICATED, (rtmStartData) => {

において渡される引数 rtmStartData に接続したチームにおける参加しているユーザー情報や所属しているチャンネルの一覧が納められているので、それを用いて処理を書けばよかった。

rtmStartData.users.forEach((v, i) => {
// ユーザーの情報でゴニョゴニョ
});
rtmStartData.channels.forEach((v, i) => {
// 所属チャンネルの情報でゴニョゴニョ
});

v4系のとき

v3系のときはRTM APIの接続周りを記述したときに自動的にユーザ情報一覧やチャンネル情報一覧を取得できていたのだけれども、このRTM API用のクライアント経由では取れなくなったので別途Web APIを呼び出してユーザ情報やチャンネル情報を取得する必要がある。従ってRTM APIで何かしら処理を行うプログラムとは別個で書くイメージになる。

蛇足だけれども、v4になって rtm.on で接続される際のイベント名は文字列で渡す様になったのでそこもnode-slack-sdkをv4にした際に併せて書き換えてあげる必要がある。

const {RTMClient} = require("@slack/client");
const rtm = new RTMClient(token, {logLevel: "error"});
rtm.on("authenticated", (rtmStartData) => {
// RTM APIを使った処理内容を書く
});
const {WebClient} = require("@slack/client");
const web = new WebClient(token, {logLevel: "error"});
web.users.list().then((response) => {
response.members.forEach((v, i) => {
// ユーザーの情報でゴニョゴニョ
});
return web.channels.list();
}).then((response) => {
response.channels.forEach((v, i) => {
// 所属チャンネルの情報でゴニョゴニョ
});
}).then(() => {
rtm.start();
}).catch((error) => {
console.log(error);
});

ドキュメントだったり、差分読めばわかるのだけど、防備録として。

loopbackで作成したプロジェクトでSlackログインできる様にする

f:id:hideack:20171126122221p:plain先日から試行錯誤していた内容のまとめ。

いまloopback.ioを利用してAPIを作ることに取り組んでいるのだけれども、frameworkが提供するユーザ認証の仕組み(メールアドレス及びパスワード)だけではなく、Node.js向けの認証ミドルウェアであるPassportを利用して他の認証機構を利用しようという取り組み。

loopback + passport でfacebooktwitterのアカウントを利用したソーシャルログインの話題は散見するのだけれども、passport-slackを使ってloopbackフレームワークで作成するプロジェクトでのユーザ認証をする話をあまりみかけず、あれこれ試行錯誤した...。

基本的にloopbackプロジェクトが用意しているpassportを利用したユーザ認証のサンプルが以下にあるのでこれに倣えばよい。

github.com

ただサンプルと作り始めているプロジェクトの差分がどこなのだ問題がはっきりせず苦労したので、主に差分を中心にまとめます。

大前提

https://api.slack.com/apps を開いてユーザ認証に使いたいアプリケーションの情報を追加する。

Redirect URIにローカルで開発する用に http://localhost:5000/auth/slack/callback といった形で登録しておく。併せてApp Credentials から Client ID と Client Secret を控えておいて、.env に以下の様な形で書いておく。

SLACK_CLIENT_ID=12345.6789
SLACK_CLIENT_SECRET=xxxyyyzzz

passportでユーザ認証させるために必要なnpmパッケージを導入

先のサンプルで加えられているnpmパッケージと、Slackログインさせるためのpassport-slackをインストールする。

$ npm install --save connect-ensure-login
$ npm install --save cookie-parser
$ npm install --save express-flash
$ npm install --save express-session
$ npm install --save loopback-component-passport
$ npm install --save loopback-passport
$ npm install --save passport-slack

passportが各種情報を収めるモデルを用意する

https://github.com/strongloop/loopback-example-passport で追加されてるモデルを追記する。

上のモデルをloopbackがデフォルトで作成するUserモデルに紐付ける

common/models/user.json

{
"name": "user",
"plural": "users",
"base": "User",
"properties": {},
"validations": [],
"relations": {
"accessTokens": {
"type": "hasMany",
"model": "accessToken",
"foreignKey": "userId"
},
"identities": {
"type": "hasMany",
"model": "userIdentity",
"foreignKey": "userId"
},
"credentials": {
"type": "hasMany",
"model": "userCredential",
"foreignKey": "userId"
}
},
"acls": [],
"methods": {}
}

上の一通りのモデルの設定を server/model-config.json に追記する。

 {
"_meta": {
"sources": [
"loopback/common/models",
"loopback/server/models",
"../common/models",
"./models",
+      "./node_modules/loopback-component-passport/lib/models"
],
"mixins": [
"loopback/common/mixins",
"loopback/server/mixins",
"../common/mixins",
"./mixins"
]
},
"User": {
"dataSource": "db",
"public": false
},
"AccessToken": {
"dataSource": "db",
"public": false
},
+  "userCredential": {
+    "dataSource": "db",
+    "public": false
+  },
+  "userIdentity": {
+    "dataSource": "db",
+    "public": false
+  },

server/server.js に追記

loopbackのcliツールであるlbコマンドで作ったプロジェクトでデフォルトで作成されたファイルをサンプルのものに置き換える。

ここで今回自分は設定ファイルをJSONではなくてJSで取る様にしたので以下の様に修正した。(.envからOAuthのパラメータ取ってもらいたかったので)

var config = {};
try {
config = require('../providers.js');  // JSにしただけ
} catch (err) {
console.trace(err);
process.exit(1); // fatal
}

Slackログイン用の各種情報を書いたproviders.js を置く

.env に記載したClient IDとClient Secretが差し込まれる様にしている。

module.exports = {
"slack-login": {
"provider": "slack",
"module": "passport-slack",
"clientID": process.env.SLACK_CLIENT_ID,
"clientSecret": process.env.SLACK_CLIENT_SECRET,
"authPath": "/auth/slack",
"callbackURL": "/auth/slack/callback",
"callbackPath": "/auth/slack/callback",
"successRedirect": "/",
"failureRedirect": "/signin",
"scope": ["identity.basic", "identity.email", "identity.avatar", "identity.team"],
"failureFlash": true
}
};

ここまでできたらloopbackのプロジェクト立ち上げて /auth/slack にアクセスすると一度Slackへ遷移したのち認証に成功するとコールバックしてくるのが確認できる。

テキストファイルに列挙されているURLを利用して該当サイトのトップページHTMLをwgetで保存する

メモ用エントリ。

http://www.storyboards.jp/

の様なURLが列挙されたテキストファイルが手元にあったときに wget を利用してトップページのHTMLファイルを特定のディレクトリに保存することをしたいと思ったので数少ない知識を活用して試してみた。

xargswget でこんな感じで試してみたのだけれどもどうだろう。MacOSだとデフォルトで入っている xargs だとGNU拡張オプションの -a のオプションが使えなかったのでbrew

$ brew install findutils

で、 gxargs を入れて以下の様な形。

$ gxargs -a urls.txt -I {} wget -t 1 -T 3 -w 0.5 -O archives/{}.html {} --user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36"

こうすることでURL一覧のテキストファイルで与えたサイトのトップページのHTMLを一つのディレクトリに保存していくことができる。

readlineで標準入力を受け付ける(対話型シェル)

Node.jsで書いたコマンドラインツールに対話型のシェルを設けたかったので試してみた。
(参照にしたgistほぼそのままなのだけれども)

アロー関数で書き直そうとしたのだけれども詰まったので一旦メモとして残しておく。

var readline = require('readline');
function Cli(handler) {
this.handler = handler;
}
Cli.prototype.run = function() {
var self = this;
var rli = readline.createInterface(process.stdin, process.stdout);
rli.setPrompt('> ');
rli.on('line', function(line) {
var args = line.split(/\s+/), cmd = args.shift();
if (self.handler[cmd]) {
self.handler[cmd].call(rli, args, function(err, res) {
console.log(res);
rli.prompt();
});
} else if (cmd.length > 0) {
console.log('cmd not found.');
rli.prompt();
} else {
rli.prompt();
}
rli.prompt();
}).on('close', function() {
console.log('');
process.stdin.destroy();
});
rli.prompt();
};
function Handler() {}
Handler.prototype.echo = function(args, fn) {
fn(null, args);
};
(new Cli(new Handler())).run();

参照

synapticを使ってXORをニューラルネットワークで再現する

ニューラルネットワーク自作入門を読んでいてサンプルはPythonで書かれているのだけれども、Nodeで書けないかなと思って調べてみたらsynapticというnpmがあったので本で読んだことを踏まえつつXOR(排他的論理和)を再現してみることにした。

www.npmjs.com

XORを再現するために入力層2段, 隠れ層3段, 出力層1段でニューラルネットワークを構成してみる。

var synaptic = require('synaptic');
var Layer = synaptic.Layer,
Network = synaptic.Network,
Trainer = synaptic.Trainer;
function Perceptron(input, hidden, output)
{
// create the layers
var inputLayer = new Layer(input);
var hiddenLayer = new Layer(hidden);
var outputLayer = new Layer(output);
// connect the layers
inputLayer.project(hiddenLayer);
hiddenLayer.project(outputLayer);
// set the layers
this.set({
input: inputLayer,
hidden: [hiddenLayer],
output: outputLayer
});
}
Perceptron.prototype = new Network();
Perceptron.prototype.constructor = Perceptron;
var myPerceptron = new Perceptron(2,3,1);
var myTrainer = new Trainer(myPerceptron);
var learning = [
{input:[0,0], output:[0]},
{input:[1,0], output:[1]},
{input:[0,1], output:[1]},
{input:[1,1], output:[0]}
];
var parameters = {
iterations: 2000,
shuffle: true,
cost: Trainer.cost.MSE
};
var results = myTrainer.train(learning, parameters);
console.log(`Error rate = ${results.error}`);
// calculate
console.log(`xor(0,0) = ${myPerceptron.activate([0,0])}`);
console.log(`xor(1,0) = ${myPerceptron.activate([1,0])}`);
console.log(`xor(0,1) = ${myPerceptron.activate([0,1])}`);
console.log(`xor(0,0) = ${myPerceptron.activate([1,1])}`);

上を実行すると以下の様にXORを再現できている。(2入力の双方に同値が入れば0に近くなり、異値が入れば1に近くなっている)

$ node xor.js
Error rate = 0.004983333135247337
xor(0,0) = 0.051013797211575035
xor(1,0) = 0.932949883533232
xor(0,1) = 0.9341209212942756
xor(0,0) = 0.08390604881521503

上のプログラム中では学習回数を2000回にしてあるけれども各回数で試行に対しての誤り率をプロットすると以下の様な感じになった。1000回超えると収束していっているのがわかる。

学生時代に簡単にニューラルネットワークについて眺めるくらいはしていたり、今回買った本を読んで原理についてはわかったつもりなのだけれども実際に自分で試してみると学習された様子がわかって面白い。何一つXORのロジックを書いてないのにXOR回路が再現できている。

参考図書

参考サイト

postd.cc

Grenacheを利用してマイクロサービスを管理&呼び出ししてみる

BITFINEXというビットコイントレードのプラットフォームを提供している会社がGrenacheというマイクロサービスを管理するフレームワークを提供しているので少し触ってみた。

例によってサンプルプログラム、ほぼそのままなのだけど多少咀嚼した(つもり)。

Grenache is a DHT based high-performance microservices framework by Bitfinex. Its decentralised and optimized for performance. Because its simple, it is easy to understand and to set up.

(ref: https://github.com/bitfinexcom/grenache )

github.com

雑にまとめたのが下の図。

特徴としては、サービスを一箇所に登録するのではなく離散ハッシュテーブルで管理されるクラスタに登録できるところで、そこにサービスを登録することでそのサービスのIP等の接続情報が管理されていくところにある。

f:id:hideack:20170701181107p:plain

まずはGRAPEと呼ばれるデーモンを立ち上げてクラスタを構成する。npmで雑にインストール。

$ npm i -g grenache-grape

GRAPEを立ち上げる。3クラスタ構成にしてみる。

$ grape --dp 20001 --aph 30001 --bn '127.0.0.1:20002'
$ grape --dp 20002 --aph 30002 --bn '127.0.0.1:20003'
$ grape --dp 20003 --aph 30003 --bn '127.0.0.1:20001'

dpクラスタ間でのDHTを構成する際に通信するためのポート番号、aphAPIのポート番号になる。 bnは立ち上げ時に参照するGRAPEを指定する。

これでクラスタは構成されたので次は実際にWorker(=サービス)を登録していく。サンプルではフィボナッチ数列の和だったけど、ここでは階乗を求めるサービスにしてみる。

GRAPEクラスタへWorkerを登録したり、登録したWorkerを呼び出すClientを作成するためのライブラリもnpmに登録されているのでこれを利用していく。

まずはWorkerから。

'use strict'
const { Link, PeerRPCServer }  = require('grenache-nodejs-ws')
function fact (n) {
if (n == 0) {
return 1
}
let m = fact(n-1);
return m * n;
}
const link = new Link({
grape: 'http://127.0.0.1:30001'
})
link.start()
const peer = new PeerRPCServer(link, {})
peer.init()
const service = peer.transport('server')
service.listen(1337)
setInterval(() => {
link.announce('fact_worker', service.port, {})
}, 1000)
service.on('request', (rid, key, payload, handler) => {
const result = fact(payload.number)
handler.reply(null, result)
})

なんとなく見て分かる通り fact_worker という名前でサービスをGRAPEクラスタに登録している。リクエストが来たときに fact というfunction呼び出してその結果をリプライしているという至極シンプルな形。

では、上のWorkerを立ち上げたままの状態で次にこれを実際に呼び出すクライアントを書いてみる。

同じく先のgrenache-nodejs-wsを利用して、

'use strict'
const { Link, PeerRPCClient }  = require('grenache-nodejs-ws')
const link = new Link({
grape: 'http://127.0.0.1:30001',
requestTimeout: 10000
})
link.start()
const peer = new PeerRPCClient(link, {})
peer.init()
const payload = { number: 10 }
peer.request('fact_worker', payload, { timeout: 100000 }, (err, result) => {
if (err) throw err
console.log(`${payload.number}! = ${result}`);
});

上を実行すると

が行われて、

$ node client.js
10! = 3628800

といった結果が得られる。

クライアントはWorkerの接続先を直接知ること無く、GRAPEクラスタに登録された階乗の結果を得るサービスの接続先情報をクラスタから得た上でWorkerに接続を行い処理結果を得ることができた。

確かにこの仕組があるとシンプルにクラスタを形成することもでき、更にそこに対してマイクロサービスを登録していくことが実現できそう。BITFINEXではこれを実際にサービスに投入しているとのことなので面白い仕掛けだなと思った。

参照

package.jsonでnpm runさせるときに記載するパスについて

package.json 中に scripts を定義してその下に実行させたいスクリプトを指定するとnpm run で実行できる。

例えば、

{
"name": "foobar",
(snip)
"scripts": {
"lint": "eslint lib/*.js bin/*",
"test": "npm run lint && mocha --require ./test/helper.js"
},
....

といった内容があると仮定すると npm run lint を実行することで記載したeslintを実行できる様になる。

で、このときにeslintなどを呼び出すときにグローバルにインストールしていなくてnode_modules以下にある場合のケースを想定して

    "lint": "./node_modules/.bin/eslint lib/*.js bin/*",

など書いていたりしたことがあったのだけれども、npm run される際にはnode_modules以下も自動的にパスが通された状態になるので明記しなくても大丈夫ですよ。

ということに今日気づいたというメモエントリーでした。

鶏そぼろの作り方, slack-cli-stream – 徒然日記

鶏そぼろ

晩御飯にそぼろご飯作る。鶏そぼろと卵そぼろを作って好きなだけお茶碗によそいだご飯にかけて食べる方式。雑に以下の様なレシピ。

  • 鶏肉ミンチ 200g
  • 醤油 大さじ2
  • 砂糖 大さじ1.5〜2
  • みりん 大さじ1
  • 水 1/4カップ

作り方は意外と簡単で上の材料一式、肉も含めて全てを 火にかけていないフライパン に全て入れてよく混ぜて中火にかける。そしてこれでもかという位、菜箸数本を束ねてとにかく混ぜるとできあがる。

火にかける前の段階でよく混ぜることが均等でいい塩梅のそぼろになるコツ。

まとめて眺める君

作っていた「Slackで自分が参加しているチャンネルをまとめてターミナルで眺めることができる君」、ひとまずgithubリポジトリに投げ込んでnpmにも登録してみた。荒削りだけど動くちゃ動く。

拾いきれてないイベントがまだいくつかあるので盆栽ワークスとしてちょこちょこ作っていこうと思う。

github.com

SlackのReal Time Messaging APIで得られる情報を眺める

自身のSlackアカウントが受領している情報を1箇所に集めてナニカできるかなと思ったとき、まず雑に情報だけ取得してみようと思い試してみた。

github.com

というのがあってnpmでも公開されているのでnpm installする。

$ npm install --save @slack/client

サンプルにもある通りなのだけれども一応さらっておくとここで取得できるトークンを利用して雑に以下の様なコードを実行するとReal Time Messaging APIで取得できる情報が全てダンプされる。

var token = 'xoxp-******-******-*****-*****';
var slack = require('@slack/client');
var RtmClient = slack.RtmClient;
var RTM_EVENTS = slack.RTM_EVENTS;
var CLIENT_EVENTS = slack.CLIENT_EVENTS;
var rtm = new RtmClient(token, {logLevel: 'debug'});
rtm.start();
rtm.on(RTM_EVENTS.MESSAGE, function (message) {
// 例えばメッセージを検出したときにはここでナニカする
});

実行すると以下の様な形で取得された情報のJSONが流れていく。

debug: { '0': 'raw_message', '1': '{"type":"pong","reply_to":2032}' }
debug: sending message via ws: {"type":"ping","id":2033}
debug: {"type":"message","channel":"C03QHFYDB","user":"U02B59NCD","text":"てすと","ts":"1494125387.277986","source_team":"T02B3CR15","team":"T02B3CR15"}

ログレベルを debug にしているので全てのRTM APIで得られたJSONが出力されている。

チャンネル上で発言された内容や別で立ち上げていたクライアントがサーバー側と接続状態を確認するためのping-pongだったりが眺められたりして素朴に面白い。

WordPressのプラグイン中で外部API等へヘッダにAuthorization入れたHTTPリクエストする

WordPressで独自のプラグインを作成しているときに、そのプラグイン中から外部のAPIなどにHTTPリクエストをする際にWordPressでは以下の様なHTTP APIのヘルパー関数が用意されているのでリクエストを発行できる。

例えばGETリクエストであれば、 wp_remote_get() という関数があるのでこちらを利用すればよい。

外部で公開されているAPIなどで認証のためにリクエストヘッダに Authorization を含める必要がある場合もこの関数の第二引数でヘッダを指定することができるのでそこを指定することで解決できる。

<?php
$token = "(API request token)";
$url = "https://api.hogehoge.jp/v1/bar/123.json";
$response = wp_remote_get($url, array('headers' => array('Authorization' => "Bearer ". $token)));

こうすると以下の様なレスポンスが得られる。

array (size=6)
'headers' =>
object(Requests_Utility_CaseInsensitiveDictionary)[610]
protected 'data' =>
array (size=11)
'server' => string 'nginx/1.4.4'
'date' => string 'Sat, 07 Jan 2017 12:47:42 GMT'
'content-type' => string 'application/json; charset=utf-8'
'status' => string '200 OK'
'x-frame-options' => string 'SAMEORIGIN'
'x-xss-protection' => string '1; mode=block'
'x-content-type-options' => string 'nosniff'
'x-request-id' => string '5f1d6608-8e5f-4fee-b584-90ffb230ece9'
'x-runtime' => string '0.056044'
'body' => string '{.....}'... (length=4209)
'response' =>
array (size=2)
'code' => int 200
'message' => string 'OK' (length=2)
'cookies' =>
array (size=0)
empty
'filename' => null
'http_response' =>
object(WP_HTTP_Requests_Response)[606]
(snip)

レスポンスヘッダを始めとして必要な要素は全て得ることができる。

あとは必要に応じてbodyに含まれる文字列要素を操作すればよい。