Hexo に Twitter の アカウント を 設定

Twitter の アカウントを開設した ので、いよいよコメント欄 Disqus の 設置!と、行きたいところですが、ちょっと寄り道. Hexo の デフォルト・テーマ Landscape に Twitter の 設定項目があるので設定したいと思います.

作業環境

  • Hexo 3.2
  • Twitter

Landscape の Twitter 設定項目

デフォルト・テーマ Landscape の 設定ファイルは /themes/landscape/_config.yml に なります. 以下に抜粋したとおり twitter: が あります.

1
2
3
4
5
6
7
# Miscellaneous
google_analytics:
favicon: /favicon.png
twitter:
google_plus:
fb_admins:
fb_app_id:

設定値 と 効果 は?

Landscape の ドキュメント Configuration – hexo-theme-landscape によると、”twitter - Twiiter ID” と あっさりしすぎてて、何のことか、また何を入れるかわかりません. 困った…

情報がないか探してみると、Configuration – hexo-theme-bootstrap-blog
に “twitter_id - Twitter ID of the author (ie. @c_g_martin)” が ありました.

異なるテーマなので同じで大丈夫かと思いましたが、”The default Landscape Hexo theme was used as the starting point - Development“ と あるので信じて設定したところ、どうやら動作したようです.

なお、設定値は以下のように、アットマークを付けシングルクォートで囲む必要があります.

1
2
3
4
5
6
7
# Miscellaneous
google_analytics:
favicon: /favicon.png
twitter: '@username'
google_plus:
fb_admins:
fb_app_id:

さっそくローカルで確認したのですが、特に変化はなかったようです… あれ?
Twitter の アイコンが増えて、リンクしてくれたりするのかなぁぐらいに思っていたのですが、どうやら違うようです. では、何が設定されたのか…

Twitter Cards !

とりあえず生成されたサイトのソースを確認します. すると head に Twitter 関連のメタ情報が出力されていました.

この出力内容は Twitter Cards に 関するもので、Twitter に サイトのリンクをツイートされた際に、以下のようにサイトの画像サムネイルやタイトル、先頭分のサマリが表示されます.


Twitter Cards の 説明によると、”リッチメディアをツイートに添付してウェブサイトへのトラフィックを促進できます – Twitterカード — Twitter Developers” とのこと.
ただの文字列リンクより、サマリが表示されるので設定しておくに越したことはないので、このまま設定しておきたいと思います.

Twitter や GitHub の アカウントへのリンクを貼る機能と勝手に思っていましたが早とちりでした. しまった…
とはいえ、リンクは貼りたいので、どっかでテーマを変えるなり、カスタマイズしないとだなぁ~

Slack で プレミアムフライデー・ボット してみる

2017年2月24日 金曜日、それは “プレミアムフライデー” なる施策の開始日である. うん、ウチは関係ないらしいのだけど! とうことで、関係ないボット を Slack に 乗せてみた.

作業環境

  • Slack
  • Node.js 6.9.1 LTS
  • Botkit 0.4.9 0.5.1
  • node-cron 1.2.1
  • moment-timezone 0.5.10

プレミアムフライデーって、なにそれ?おいしいの?

まずもって関係ないので、よく知りません. 人によっては美味しいものであったり、おもしろい物であるようです.

経済産業省によりますと以下の施策だそうです.

個人が幸せや楽しさを感じられる体験(買物や家族との外食、観光等)や、そのための時間の創出を促すことで、
(1) 充実感・満足感を実感できる生活スタイルの変革への機会になる
(2) 地域等のコミュニティ機能強化や一体感の醸成につながる
(3)(単なる安売りではなく)デフレ的傾向を変えていくきっかけとなる
といった効果につなげていく取組です。

なんか漠然として、よくわかりませんが、こちらのサイト を サマると以下でしょうか.

  • 月末の金曜日は早く仕事を終え退社する
  • 早く退社した時間は、余暇として楽しむ
  • ちょっと豊かな月末金曜日を過ごしてハッピーになる

いいなぁ、ハッピ~. こちらもアバウトな感じでよくわからない.
とりあえず聞いたことある話からすると、以下のようです.

  • 月末の金曜日は 15時までに退社する
  • 退社後は食事や娯楽などを楽しむ
  • 消費が活性化され、楽しんだ人 も 景気 も ハッピー になる

なるほど. いいなぁ、ハッピ~.

ボットの仕様を考える

月末の金曜日は 15時に変えることを促す通知をするのが通常仕様とすることになりそうです. 一方で関係ない身としては 15時に帰れと言われても困りますので、何かしらの工夫が必要です.

いただきます! こちらのネタ!!
ということで、まずは usernameicon を プレミアムフライデーなアカウントにして 15時退社をアナウンス. 続いて通常ボット・アカウントに戻って上記ネタでリプライする、自作自演ボットにしたいと思います. これなら関係無い感が出てるボットになって、きっと朝から はっぴー な 気分になれ…

ボット実装

自動切断のワークアラウンド・コードを削除する更新をしました (2017年3月10日)

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
'use strict';
const Botkit = require('botkit');
const cron = require('cron');
const http = require('http');
const moment = require('moment-timezone');

const controller = Botkit.slackbot();
moment.locale('ja');
moment.tz.setDefault('Asia/Tokyo');

controller.spawn({
token: process.env.bot_access_token
}).startRTM((err, bot, payload) => {
new cron.CronJob({ cronTime: '00 00 11 * * 5', timeZone: 'Asia/Tokyo', start: true, onTick: () => {
let now = moment();
if (now.month() != now.clone().add(7, 'days').month()) {
bot.say({
channel: 'random',
text: '今日、月末の金曜日は午後3時に退社して余暇を楽しもうという ' +
'<https://premium-friday.go.jp/#section_about|プレミアムフライデー> だよ! ' +
'退社時間が早まることで消費が活性化するし、働き方改革にもつながる施策なんだ. ' +
'さぁ~ みんな3時には帰って、買い物、食事や旅行などして普段よりも豊かな生活を送ろう!!',
username: 'プレミアムフライデー ボット',
icon_url: 'http://www.meti.go.jp/press/2016/12/20161212001/20161212001-a.jpg'
}, (err, response) => {
bot.say({
channel: 'random',
text: `<http://[YOUR_IMG_URL]/image.jpg?${moment().unix()}| >`
});
});
}
}});
});

Botkit や Moment.js の セットアップ系はいつも通りで、定時処理は毎度の new cron.CronJob() で ジョブを作成します.
今回は「月末の金曜日 11時」に 処理をしたいのですが、cronTime だけでの表現が浮かびませんでしたので、まずは「金曜日 11時」に 処理を起動するようにしました.
続いて現在日時を取得して、7日後が同じ月か now.month() != now.clone().add(7, 'days').month() で 判定することで月末の金曜日なのかを確認しています.

月末の金曜日だったら、まずは「プレミアムフライデー ボット」が 広報の通知をするために usernameicon_url を 変更してポストします.

「プレミアムフライデー ボット」が 発言した後に返事を返したいので、以下のように bot.say() の 中で再発言させています. bot.say() を 並べてしまうと、タイミングによっては通常ボットの返事が先に来てしまうことがありえるので、bot.say() の コールバックから発言する処理になります.

1
2
3
bot.say({ /* プレミアムフライデーの発言 */ }, (err, response) => {
bot.say( /* 通常ボットからの返事 */ );
});

通常ボットからの返事では、<http://[YOUR_IMG_URL]/image.jpg?${moment().unix()}| ><url| > の 書式で URL を 半角スペースに置き換えてポストする記法を使っています. これによって URL 無しで画像だけポストできます. ?${moment().unix()} は、Slack が 同じ URL だと 2回目以降は折りたたんでしまって、最初から画像が見えないので、実行時の Unix timestamp を つけることで URL を 変えて展開できるようにしています.
※ 画像は自前で用意して適当なところへ置いておきます.

実験!

cronTime を 調整して、いざ実験. ちゃんと表示されました.
とりあえず実験用にはネタ元のたツイートから画像を拝借させて頂きましたが、後は画像を自前のネタに変えて適当なとこにアップしてボットを設置するだけですね. 関係ない人々の月末の週末が楽しみです♪



今週末に迫ってきたプレミアムフライデー. 急に騒がしくなってくる中で関係ない身としては流れに乗れない寂しさから勢いボットを作って乗ってみました. 帰るころにはハッピーな人で溢れかえっているか、後の祭りか…
みなさま、よい週末を.

『知識ゼロから学ぶソフトウェアテスト』 あるいは 無残なるノーテスト

Java で プログラミングをしている場合に、テストケースの作成に JUnit を ベースにテストしているケースがあるかと思います. “Assert that X is Y.” の 考え方 を 基本に isnotnullValue などしてテストケースを書いていくよいライブラリと思うのですが…

テスト も カバレッジ も ちゃんと やってます!

それは昔々の事、とあるアプリケーションに携わった人のお話しで機能追加することから物語は始まりました. (いや物語らん話しのがよかた)

このアプリケーション、しっかりテストケースが作ることになっており、なんと C0 カバレッジ 90% 以上、C1 も 努力目標ながら高レベルでされているとのことでした. ちゃんとテストするフレンズなんだね!すごーい!(2017年2月現在 流行りのフレーズ)

と、聞いていたのも束の間、コードを触り始めると気になる謎の実装、不思議なテストケース…
「テストなってないじゃん?」いや、まぁ、「テストするとは」の定義は何かと自問自答するわけですが、テストケースがあるとか、カバレッジが取れてるとかでなくて、「何を検証したいのか」じゃないのかなぁと、考えたとのことです.
僭越ながら同感です. いたずらにカバレッジだけとっても、通過させるためだけの意味のないテストケースの実行コードなんてものがあふれることもあり得ます.

え? その “verify(x, never()).y()”…

さて、そんな中、聞かれたのが “verify(x, never()).y()” というコード. これだけ聞かれてもよくわからないのですが、たぶんモックでメソッドの呼び出し有無を判定したいと思われます.

そして出てきたのが以下のようなテストケース. (※ ポスト用に命名等を汎化させています)

1
2
3
4
5
6
public class DaoTest {
@Test public void testAdd() {
verify(dao, never()).findAll();
}
// ...(省略)
}

いゃ~! 見たくなかった、聞きたくなかった…
たぶん、Deta Access Object = DAO の テストケースと思われます.
きっと、新しいレコード を 追加 = add するのをテストしたかったのでしょう. 知らんけど.
verify()never() は モックを簡単にするためのライブラリから、メソッドの実行確認をしているのでしょう.
だけど、dao.add() 実行してないじゃん? あと findAll() しなくね?、どう考えても. (そもそも Dao の 処理を何も実行してないからアレだけど) White Box テストだから知ってるはずだし?

どうして、そうなったのか. 少し話を聞いたたらしいのですが、「テストって、何をテストをするんですか?」「どうやるんですか?」「できすか?やって見せてください!」…
えーっと、逆ギレ? DAO とかテストめんどくさいの分かるけど、単純 な Getter/Setter だけのテストだってできてないんですが (T_T

1
2
3
4
5
6
7
8
public class ModelTest {
@Test public void setParam() {
Model model = new Model();
model.set("あああ..."); // ※ すごい長い「あ」が続く
assertEquals("あああ...", model.get());
// ※ この後、同様のパターンで半角英数、記号、全半角の組み合わせ? などが続く
}
}

なお、C0 90% は、どっか別のテストケースで実行していたのが、たまたま通過していたものだったとか. 対となるテストケースの実行では無残なカバレッジ率とのこと… はあぁ 全部作り直しかよ~と、ご愁傷様です ( ̄人 ̄)
ひゃ~ こわい、こわい.

“assertTrue(true)” ?

そんな今となっては香ばしい昔話を黄泉がえらせたのが、Titter に 流れてきた衝撃のツイート.

“assertTrue(true)”… 何を言っているのかわかりませんでいた. いや TDD とかで、Fake it、ですよね~ やるやる~~www と…

いや、ヤバいです. どういう話しですか?
先の昔話も 今も テストの闇は深い… (“JUnit 目視” という、別系統のヤバいのも見た気が)

『知識ゼロから学ぶソフトウェアテスト』

テストで悩んだら『知識ゼロから学ぶソフトウェアテスト』を 読んでみるのがよいかと思った次第で、ちょうどアプリケーションのテストの不備に、”assertTrue(true)” という闇の呪文、半額セールが重なったので、勢い投稿を書いてしまいました!

こちらの書籍は JUnit とかの実装技術ではないのですが、「テストとは」という視点で書かれています. テストに対する考え方を学ぶことができるのではないでしょうか. いきなりテストケースを実装する前に、何を、どうやってテストするのかを知ってから、作るとよいかと思います.

私は改訂版の前の書籍を読んだことがありますが、テストケースはどう作るべきかの基礎をしっかり学ばせていただきました. 改訂版とのことで、どう変わったのかは改めて読みたいと思いますが、基本的な考え方を知るにはよいと思います.


「テストって何をするんだろ」には『知識ゼロから学ぶソフトウェアテスト』も よいと思いますので、よかったら ご参考までに.

Twitter の アカウント作成

以前 WordPress を 使っていたことがあり、それに比べて Hexo と GitHub は 簡単にブログを設置 できるなぁと、WordPress 界隈が騒がしい今日この頃、ふと WordPress 時代を思い出しました.

改めて思い出すことなく、前々から気にはなっていたのですが Hexo にはコメント欄がありません. 静的ジェネレータで生成されたサイトなのでサーバ側の処理がないので当然と言えば当然なのですが、やはり寂しい.

作業環境

  • Hexo 3.2
  • Twitter

Hexo の コメント欄

いろいろと調べてみると Hexo 2.x のころにはコメント欄があったようで、3.x になってからテーマとの兼ね合いからなくなったのだとかなんとか.
現在は Disqus というサービスを使うと設置できるようなので、さっそく設置に取り掛かろうと思ったところ、SNS の 認証連携は Facebook, Twitter, Google+ で、GitHub は 対応していませんでした.

メールで直接サインアップするよりも、SNS に まとめておきたいので、今回は Twitter の アカウントを作成し、そのアカウントから Disqus へ サインアップしたいと思います.

Twitter の アカウント作成

Twitter の Signup ページ https://twitter.com/signup へ アクセスします.

呼び名、メールアドレス、パスワード を 入力し、[アカウント作成] ボタンをクリックします.
ここの “呼び名” は、Twitter での表示名で、既に使われている名前とも重複可能です. また後程変更することもできます. ネット上でのハンドルや、いつものニックネームで大丈夫です.

電話番号の入力を求められますが、特に必要なければ [スキップ] を クリックします. 今回はスキップしました.

ユーザー名、アカウント名を入力します. こちらは Twitter 内 で 一意となる文字列が求められます. “2006年7月にオブビアウス社(現Twitter社)が開始したウェブサービス – Wikipedia“ と 約10年の歴史に、Twitter, Inc.について | About の 2017年2月現在 で 約 3億 月間アクティブユーザー とのことで、なかなか思うアカウント名は取れません… (画面はブログ用に加工してます)

無事、アカウントが取れると Welcome 画面が表示されます. [さぁ、はじめよう] を クリックし進めます.

興味あることからフォローするアカウントなどが勧められたりするようですが、今回は何もせずに [続ける] しました. 明確に取りたい情報があれば設定しておいてもよいかもしれません.

アドレス帳のアップロードを求められますが、今回は [利用しない] で 先へ進めました. いろいろなサービスがある中で、何と、どこまでを、リアルとくっつけるのかは悩ましいところです.

フォローするアカウントの選択として、いくつかのアカウントをサジェストしてくれます. 日本語で日本からサインアップしているのですが 2017年の話題の人としてプレジデントを勧めてくれたのでしょうか… 画面に表示された “場所に基づくおすすめ “ 感 が あまりないのですが.

ブラウザのポップアップ通知について聞かれます. 必要な方を選択します. 使ったことは無いのですが、専用クライアントなしでブラウザだけで通知できるとは技術の進歩は素晴らしいですね.

ようやく、Twitter を 利用できるようになりました. “さぁ、はじめよう” って、言ってたのに長かった.


ブログのコメント欄 Disqus に向けて、一歩を踏み出すことができました. Twitter 自体も用意したいなぁと思っていたので、ちょうど設置できてよかったです.
しばらくは ツイート できないとは思いますが、せっかく設置できましたので @azriton も よろしくお願いいたします.

Hexo の 新規投稿パス を カスタマイズ

Hexo では 新しい投稿は hexo new "記事タイトル" コマンドで作成します. その際に新しいファイルは /source/_posts/記事タイトル.md として作成されます. 初めのうちはひとつのフォルダ内でよいのですが、ファイルが増えてくると管理に困ってしまいます.
このファイルが作成される場所を管理しやすいように Hexo を 設定したいと思います.

作業環境

  • Windows 7
  • Hexo 3.2

新規投稿ファイルの配置場所

この設定は _config.ymlnew_post_name に なります. 以下に初期設定を抜粋したように :title.md と なっているため、/source/_posts に コマンドの引数で与えた 記事のタイトル.md というファイルが作られます.

1
2
# Writing
new_post_name: :title.md # File name of new posts

ここの設定を変えることで、作成されるファイルをコントロールすることができます. パーマリンクの設定を例にみると permalink: :year/:month/:day/:title/ のように :year などの変数を使っています.
この例に倣って、以下のように年月フォルダにファイルを置くように設定してみます.

1
2
# Writing
new_post_name: :year/:month/:title.md # File name of new posts

hexo new "記事のタイトル" を 実行したところ、ねらったとおりに年月フォルダに作られるようになりました.
ここで使える変数についてのドキュメントは見当たらなかったのですが、Permalinks | Hexo を 参考に試してみました. :i_month:i_day などは動作しました. :id:categoryundefined というフォルダになります. カテゴリは、この時点では設定がないので当然ですね.

新しいフォルダへの引っ越し

これまでのファイルは自分でフォルダを作成する必要があります. フォルダを作成してファイルを移動することで完了です.
ここは自動でできないので致し方ありません.

ところで、

実は new_post_name の フォルダ部分 は ファイルを作成する際に参照される設定で、サイトの生成には関係していません.
そのため、これまでのファイル は そのままに、新しく作るものだけを新しい設定のフォルダで管理することもできますし、全く関係ないフォルダ名で管理することもできます. もちろん、日本語のフォルダ名も利用できます… (あとから気づいた orz)
そうなると内部的なカテゴリで分けても良かったのかもと思ったりもします.

ただ、ファイル名については URL で 使われるので、こちらに細工するこは URL として見えてしまうことを前提にする費用があります.
フォルダを内部的な カテゴリ や シリーズ で 整理して、ファイル名に連番を付けてもよいかと思ったのですが URL に 連番が出るのは悩みどころです.


最初にファイルを生成するときに、一定のルールに従って生成してくれるという機能の紹介でした.
ある程度ポストが増えてくるとファイル管理に意外と悩むものなんだなぁと感じます. 規模や特性によるので一概には言えないことなのでしょうが、何か良い方法があったら知りたいところです.

Slack の ボット で 定期的に降水予報を通知する

朝の天気と、その後の降水予報が Slack に 通知できるようになりました.
これで傘を持っていくかを判断できるようになりましたが、朝は晴れていて日中晴れの予報でも夕方に天気が崩れることもあります. 何てことだ orz

既に傘は持ってきていないわけですが、あらかじめ天気が崩れることが分かれば、予定を変更して早く帰るなりの作戦が取れます. (まぁ、作戦を立てたとして実行できるかは別なのですが…)

今回は、降水予報の実装 を 定期的に実行して、雨もしくは雪が降る予報があった場合に Slack に 通知するようにしたいと思います.

作業環境

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

通知方法の検討

OpenWeatherMap で 取得できる予報は、「3時間ごと 5日間」と「日次で 16日間」の 2つがあります. 無料で使えるのは「3時間ごと 5日間」なので、こちらを使って降水予報を実装したいと思います.

取得できるデータには降水確率などの情報は無いですが、天気状況に加えダイレクトに降水量があります. この降水量を使うことにして 9時間以内に降水量がある場合に、降水予報として通知するようにします.

Slack ボット の 実装

定期実行のスケジュールが異なるため、毎朝の天気情報とは異なる cron ジョブを作成します.

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
new cron.CronJob({
cronTime: '00 00 9-21 * * 1-5',
onTick: () => {
http.get(`http://api.openweathermap.org/data/2.5/forecast?id=${city}&appid=${apikey}&units=metric&cnt=3`, (response) => {
let body = '';
response.setEncoding('utf8').on('data', (chunk) => { body += chunk; });
response.on('end', () => {
let json = JSON.parse(body);
for (let i in json.list) {
let forecast = json.list[i];
if ((forecast.rain && forecast.rain['3h']) || (forecast.snow && forecast.snow['3h'])) {
let text = '→ ' + labels.next(forecast.dt_txt) + labels.desc(forecast) + labels.prec(forecast);
http.get(`http://api.openweathermap.org/data/2.5/weather?id=${city}&units=metric&appid=${apikey}`, (response) => {
let body = '';
response.setEncoding('utf8').on('data', (chunk) => { body += chunk; });
response.on('end', () => {
let current = JSON.parse(body);
bot.say({
channel: channel,
text: text,
username: `${current.weather[0].main}(${current.weather[0].description})`,
icon_url: `http://openweathermap.org/img/w/${current.weather[0].icon.replace('n', 'd')}.png`
});
});
});
break;
}
}
});
});
},
start: true,
timeZone: 'Asia/Tokyo'
});

基本的な構造は、朝の天気予報 の OpenWeatherMap API 呼び出し順が逆になるだけで、ほぼ同じです.
cronTime は 平日の 9時~21時に、1時間ごとに実行するようにしました.
API 呼び出しは、最初に /forecastcnt=3 で 3時間毎 の データ を 3回分で 約 9時間を取得しています. その中に降水情報があったら アイコン と ユーザ名 に 使う、現在の天気 /weather を 取得します.



予報も出せるようになったので、朝から傘が必要か、そして日中の天気の変化について知ることができ、降水確率 ○○% というよりも、何時に、どのくらいの雨(雪) が 降るのかが分かるので、意外とよさそう.
これまで実装してきたものを組み替えるだけで簡単に作れました. API さまさま、ですね. ありがたい.

Slack の ボット で 今日の天気を通知する - 表示最適化編

朝の天気と、その後の降水予報が Slack に 通知できるようになりました!
これで毎朝の天気を確認できるわけですが、その情報を見るツールとしてスマホを使った時に少し気になる点がありました.

作業環境

  • Slack
  • Android 6.0
  • Slack 2.27.0 (Android App)
  • Node.js 6.9.1 LTS
  • Botkit 0.4.9

スマホ表示の確認


ちゃんと表示されています! が 天気アイコン、デカい!
分かりやすいと言えば分りやすいですが、ちょっと大きすぎですね. 1日分で、ほぼ画面が埋め尽くされてしまっています. もう少しコンパクトに表示するようにしたいと思います.

表示方法の検討

Slack の ボット は 発言する際に、アイコン と 名前 を 変えることができます. (chat.postMessage method | Slack)
これを使うことで、現在ロボットのアイコンが表示されている部分 を 天気アイコン にし、名前の部分に天気情報を入れることにすれば、すっきりしそうです.

Botkit での 実装方法ですが、その辺についての説明はありませんでした. 残念.
近いところとしては Slack-specific fields and attachments に リプライにアタッチする話があります. アタッチの中に usernameicon_url が 登場しますが、アタッチしたいわけではないのでちょっと異なりそうです.

仕方がないのでソースに当たることにします.
ボットを話させているのは Slackbot_worker.js#send() – Botkit v0.4.9 です. こちらを参照すると slack_message 変数を作っているところに見事に usernameicon_url が あります. つまり、この関数の引数 message に 渡してあげることで設定できそうです. 以下に Slackbot_worker.js#send() を 抜粋.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bot.send = function(message, cb) {
botkit.debug('SAY', message);

/**
* Construct a valid slack message.
*/
var slack_message = {
type: message.type || 'message',
channel: message.channel,
text: message.text || null,
username: message.username || null,
parse: message.parse || null,
link_names: message.link_names || null,
attachments: message.attachments ?
JSON.stringify(message.attachments) : null,
unfurl_links: typeof message.unfurl_links !== 'undefined' ? message.unfurl_links : null,
unfurl_media: typeof message.unfurl_media !== 'undefined' ? message.unfurl_media : null,
icon_url: message.icon_url || null,
icon_emoji: message.icon_emoji || null,
};
// (省略)...
};

usernameicon_url の 渡し方ですが、こちらは、もうあまり考えることは無いですね. 既に channeltext を 渡しているわけですから、それらと一緒に渡すだけになります.

Slack ボット の 実装

以下にボットの発言部分 bot.say() の 実装を抜粋します.

1
2
3
4
5
6
bot.say({
channel: channel,
text: text,
username: `${current.weather[0].main}(${current.weather[0].description})`,
icon_url: `http://openweathermap.org/img/w/${current.weather[0].icon.replace('n', 'd')}.png`
});

channeltext に 合わせて、usernameicon_url を 設定するだけです.
前回は text に アイコン の URL を 使っていたので、Unix timestamp を パラメータでつけていましたが、今回は常に展開されるので Unix timestamp は 不要です.

通知!


アイコンと天気情報がコンパクトにまとまりました.
実際には “Today” の 線が引かれたりするので、さらに見やすくなると思われます.



今回はプログラムのテストで朝の情報を夜に出していますが、Slack への ポスト・タイミング と 天気予報の計算時刻は、そう大きくはずれないので “6:52 現在” などを削ってもよいかもしれませんね.

Slack の ボット で 今日の天気を通知する - 降水予報 実装編

毎朝の天気が Slack に 通知されるようになりました. これは現在の天気を通知しているので、朝の天気を知ることはできても、その後どうなるかはわかりません. 今回は、その後 雨(雪) が 降るのかの予報も合わせて通知するようにしたいと思います.

作業環境

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

通知方法の検討

OpenWeatherMap で 取得できる予報は、「3時間ごと 5日間」と「日次で 16日間」の 2つがあります. 無料で使えるのは「3時間ごと 5日間」なので、こちらを使って降水予報を実装したいと思います.

取得できるデータには降水確率などの情報は無いですが、ダイレクトに天気状況と降水量があります. この降水量を使うことにして 24時間以内に降水量がある場合に、降水予報として通知するようにします.

Slack ボット の 実装

以下に 前回実装onTick: 部分を抜粋し今回の追加分をあげます.

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
onTick: () => {
http.get(`http://api.openweathermap.org/data/2.5/weather?id=${city}&units=metric&appid=${apikey}`, (response) => {
let body = '';
response.setEncoding('utf8').on('data', (chunk) => { body += chunk; });
response.on('end', () => {
let current = JSON.parse(body);
let text =
`${moment.unix(current.dt).format('H:mm')} 現在 ${current.name} の 天気` +
`<http://openweathermap.org/img/w/${current.weather[0].icon.replace('n', 'd')}.png?${moment().unix()}| > ` +
`${current.weather[0].main}(${current.weather[0].description}) / ` +
`気温 ${Math.round(current.main.temp)} ℃ ` +
`${current.rain && current.rain['3h'] ? '/ 降雨量 ' + Math.ceil(current.rain['3h'] * 10) / 10 + ' mm ' : '' }` +
`${current.snow && current.snow['3h'] ? '/ 降雪量 ' + Math.ceil(current.snow['3h'] * 10) / 10 + ' mm ' : '' }`;

// ここから追加
http.get(`http://api.openweathermap.org/data/2.5/forecast?id=${city}&appid=${apikey}&units=metric&cnt=8`, (response) => {
body = '';
response.setEncoding('utf8').on('data', (chunk) => { body += chunk; });
response.on('end', () => {
let json = JSON.parse(body);
for (let i in json.list) {
let forecast = json.list[i];
if ((forecast.rain && forecast.rain['3h']) || (forecast.snow && forecast.snow['3h'])) {
text +=
`\n→ 降水予報 ${moment.utc(forecast.dt_txt).fromNow()} ` +
`(${moment.utc(forecast.dt_txt).utcOffset(9).format('M/D H:mm')}) / ${current.weather[0].main} ` +
`${forecast.rain && forecast.rain['3h'] ? '/ 降雨量 ' + Math.ceil(forecast.rain['3h'] * 10) / 10 + ' mm ' : '' }` +
`${forecast.snow && forecast.snow['3h'] ? '/ 降雪量 ' + Math.ceil(forecast.snow['3h'] * 10) / 10 + ' mm ' : '' }`;
break;
}
}
bot.say({ text: text, channel: channel });
});
});
// ここまで
});
});
},

予報は json.list に 入っているので for 文で 1つずつ処理します. 予報の時刻 forecast.dt_txt を 表示用にフォーマットし、後は現在の天気で処理したのとほぼ同じになります. アイコンが並ぶとわかりにくいので、予報のアイコンは不要としました.

ボットが発言する bot.say() は、予報の JSON を 取得した後になります.

通知!


朝の天気に加え 20時間後、明日 の 午前 3:00 に 雨が降るとの予報も表示されました. この予報だと傘の出番はなさそうですね.



予報も出せるようになったので、朝から傘が必要か、帰りには必要になるのかが分かるようになりました. 降水確率 ○○% というよりも、何時に、どのくらいの雨(雪) が 降るのかがはっきりするのでよいさそうです.

ちょっと気になるのが、天気の精度やデータの有無…
データやキャプチャは東京で統一するようにしたいと思っているのですが、天気の事なので思うような状態でないことはあります. その際に他の都市をいろいろと探すのですが、どうも他の天気予報と合ってなかったり、極端な話 OpenWeatherMap の 地図 とも合ってない時があるような気も…
また、天気 が Rain に なっているのに、現在の天気 API に rain.3h が 入ってなかったりと、なのに Clear で 0.1 mm とか… 不思議だ…
無料で使わせてもらっているので、多くは望まないのですが、精度が悪いようだと困るなぁ. 今度、いろいろと比較を出して調べてみよう.

Slack の ボット で 今日の天気を通知する - Slack ボット 実装編

OpenWeatherMap の サービスを使って、天気情報を取得できるようになり、JSON の 内容も確認 しました.
いよいよ Slack の ボットに組み込んで、毎朝の天気を通知してもらいましょう.

作業環境

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

通知方法の検討

天気は毎日の事なので、毎朝 7:00 に 通知してもらうことにしたいとおもいます. 土日はもう少し遅くてよいかもしれませんが… とりあえず使ってみて、調整しましょう.
通知内容は、天気、気温、降雨量(降雪量) にし、せっかくなので OpenWeatherMap の アイコン画像も表示します.

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
'use strict';
const Botkit = require('botkit');
const http = require('http');
const cron = require('cron');
const moment = require('moment-timezone');

const controller = Botkit.slackbot();

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

const city = '1850147'; // Tokyo
const channel = 'sandbox'; // #sandbox
const apikey = '[API_KEY]'; // Please replace with your API_KEY

new cron.CronJob({
cronTime: '00 00 7 * * *',
onTick: () => {
http.get(`http://api.openweathermap.org/data/2.5/weather?id=${city}&units=metric&appid=${apikey}`, (response) => {
let body = '';
response.setEncoding('utf8').on('data', (chunk) => { body += chunk; });
response.on('end', () => {
let current = JSON.parse(body);
let text =
`${moment.unix(current.dt).format('H:mm')} 現在 ${current.name} の 天気` +
`<http://openweathermap.org/img/w/${current.weather[0].icon.replace('n', 'd')}.png?${moment().unix()}| > ` +
`${current.weather[0].main}(${current.weather[0].description}) / ` +
`気温 ${Math.round(current.main.temp)} ℃ ` +
`${current.rain && current.rain['3h'] ? '/ 降雨量 ' + Math.ceil(current.rain['3h'] * 10) / 10 + ' mm ' : '' }` +
`${current.snow && current.snow['3h'] ? '/ 降雪量 ' + Math.ceil(current.snow['3h'] * 10) / 10 + ' mm ' : '' }`;
bot.say({ text: text, channel: channel });
});
});
},
start: true,
timeZone: 'Asia/Tokyo'
});

});

Slack ボット の 基本的な作りは Slack の ボット で 定時アクション の 実装と変わりがありません.
※ 今回は #sandbox へ 発言するので const channel = 'sandbox'; としていますが、ID で 指定したほうがよいので https://slack.com/api/channels.list?token=[API_TOKEN] へ アクセスして、ID を 調べます. また、発言先のチャンネルへボットが参加している必要があります.

今回は Unix Timestamp の 処理のために、moment-timezone を 使いました. Unix Timestamp から、指定したタイムゾーンでの表記にするには、moment ではなく moment-timezone が 必要となります. また、クラウド環境などでタイムゾーンが異なる環境で動作させる場合も、タイムゾーンを指定しておく必要があります. 今回はロケールに関する出力はありませんが、合わせて設定しておきます. 設定方法は以下のコードとなります.

1
2
moment.locale('ja');
moment.tz.setDefault('Asia/Tokyo');

続いて、cron.CronJobonTick で OpenWeatherMap の API へ HTTP GET して、天気情報を取得して、Botkit で 話してもらう処理になります.
天気情報 の JSON も 確認できましたので、この内容を文章化するだけになります. その辺が let text = ...; に なりますが、JSON (ここでは current 変数) から取り出してつなぐだけですが、文字列の連結なのでかなり、ごった煮状態です…

文章を作っているところを 1行ずつ確認したいと思います.

1
`${moment.unix(current.dt).format('H:mm')} 現在 ${current.name} の 天気` +

日付を扱う Moment.js を 使い、データ計算時間 の Unix 秒 を フォーマットし、${current.name} で 都市名を取り出しています.
都市名は ID を 指定している時点でわっているので明示しても良いのですが、都市 ID を 切り替えるだけで動作できるので、JSON から取り出しています.

1
`<http://openweathermap.org/img/w/${current.weather[0].icon.replace('n', 'd')}.png?${moment().unix()}| > ` +

OpenWeatherMap の アイコンを表示する部分になります. <url|text> の 形式 で ポストすることで、URL の 代わりに文字列のリンクをポストできます. 今回はリンクを出す必要がないので <url| > と スペースを入れることで潰しています. スペースなしだと URL が 表示されたのでスペースにしました.
アイコンのファイル名ですが、${current.weather[0].icon.replace('n', 'd')}.png としています. 午前 7時だと夜用アイコンが表示されるので、常に昼用のアイコンを使うようにしています.
また URL の 最後の ?${moment().unix()} ですが、Slack が 同じ画像 の URL が ポストされた際に展開されない(画像表示されない) という動作をしました. そのため 現在 の Unix timestamp を 付けています. current.dt を つけるのも手です. (開発時に困ったので Unix timestamp にしました)

1
`${current.weather[0].main}(${current.weather[0].description}) / ` +

現在 の 天気グループ と 天気状態 の 出力になります. (どちらかだけ、でもよかったかも…)

1
`気温 ${Math.round(current.main.temp)} ℃ ` +

気温です. 読みやすさ重視で、ざっくり小数点以下を四捨五入して、整数表示にしてます.

1
2
`${current.rain && current.rain['3h'] ? '/ 降雨量 ' + Math.ceil(current.rain['3h'] * 10) / 10 + ' mm ' : '' }` +
`${current.snow && current.snow['3h'] ? '/ 降雪量 ' + Math.ceil(current.snow['3h'] * 10) / 10 + ' mm ' : '' }`;

降水量です. 雨 と 雪 で データが異なり、また両方に値が入っているケースがあります. しかも、current.raincurrent.snow に 空オブジェクトが入るケースがあったので、current.rain && current.rain['3h'] と 両方を確認して、値が存在したら出力するようにしています.
また四捨五入すると 天気 が 雨 なのに 0 mm に なるケースもあるので、切り上げにしています.

通知!


天気が通知されました.



これで定時アクションの定番、毎朝の天気情報 が Slack に 通知できるようになりました!

天気情報が英語な部分が気になりますが、OpenWeatherMap は 他言語対応されているものの日本語には対応していないので、とりあえずアプリ側で対応する方法を考えてみたいとおもいます.

また単に天気情報を通知するだけでしたら、もっと良いアプリや IFTTT 連携などでもっと手軽に作れます. せっかく自前で実装したのですから、もう少し工夫を追加してみたいと思います.

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

OpenWeatherMap の サービスを使って、天気情報を取得できるようになりました.
今回は取得した JSON の 内容を確認し、必要な情報が使えるようにしたいと思います. 雨と雪の情報もほしかったので、札幌市の天気を取得しました.

作業環境

  • Node.js 6.9.1 LTS

現在の天気情報 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
{ coord: { lon: 141.35, lat: 43.06 },
weather:
[ { id: 501,
main: 'Rain',
description: 'moderate rain',
icon: '10d' } ],
base: 'stations',
main:
{ temp: 4.01,
pressure: 994.64,
humidity: 97,
temp_min: 4.01,
temp_max: 4.01,
sea_level: 1008.09,
grnd_level: 994.64 },
wind: { speed: 9.56, deg: 194.002 },
rain: { 3h: 4.905 },
clouds: { all: 88 },
dt: 1485498017,
sys:
{ message: 0.0083,
country: 'JP',
sunrise: 1485467687,
sunset: 1485502824 },
id: 2128295,
name: 'Sapporo-shi',
cod: 200 }
ノード 参考値 概要
coord { lon: 141.35, lat: 43.06 } 都市の緯度経度
weather.id 501 天気状態 の ID*
weather.main Rain 天気状態 の グループ名*
weather.description moderate rain 天気状態*
weather.icon 10d 天気アイコン ID*
main.temp 4.01 気温
main.pressure 994.64 気圧、単位は hPa
main.humidity 97 湿度、単位は %
main.temp_min 4.01 現時点での最低気温
main.temp_max 4.01 現時点での最高気温
main.sea_level 1008.09 海上の気圧、単位は hPa
main.grnd_level 994.64 地上の気圧、単位は hPa
wind.speed 9.56 風速、単位は メートル/秒
wind.deg 194.002 風向、北 0 の 時計回り
clouds { all: 88 } 曇り度合、単位は %
rain.3h 4.905 過去3時間の降雨量
snow.3h 過去3時間の降雪量
dt 1485498017 データ計算時間、Unix,UTC
sys.country JP 国コード
sys.sunrise 1485467687 日の出時間、Unix,UTC
sys.sunset 1485502824 日の入り時間、Unix,UTC
id 2128295 都市 ID
name Sapporo-shi 都市名
  • Weather condition codes に 対応表がある
    ※ API ドキュメントに Internal parameter とあるものは除く

3時間ごと 5日間 の 天気予報 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
34
35
{ city:
{ id: 2128295,
name: 'Sapporo-shi',
coord: { lon: 141.346939, lat: 43.064171 },
country: 'JP',
population: 0,
sys: { population: 0 } },
cod: '200',
message: 0.1264,
cnt: 38,
list:
[ { dt: 1485507600,
main:
{ temp: 0.15,
temp_min: 0.15,
temp_max: 0.15,
pressure: 1000.35,
sea_level: 1013.94,
grnd_level: 1000.35,
humidity: 91,
temp_kf: 0 },
weather:
[ { id: 500,
main: 'Rain',
description: 'light rain',
icon: '10n' } ],
clouds: { all: 56 },
rain: { 3h: 0.2975},
snow: { 3h: 0.1575},
wind: { speed: 8.3, deg: 258.503 },
sys: { pod: 'n' },
dt_txt: '2017-01-27 09:00:00' },
{ ... (3時間ごとの繰り返し) }
]
}
ノード 参考値 概要
city.id 2128295 都市 ID
city.name Sapporo-shi 都市名
city.coord { lon: 141.346939, lat: 43.064171 } 都市の緯度経度
city.country JP 国コード
cnt 38 レコード件数
list.dt 1485507600 データ計算時間、Unix,UTC
list.main.temp 0.15 気温
list.main.temp_min 0.15 計算時の最低気温
list.main.temp_max 0.15 計算時の最高気温
list.main.pressure 1000.35 気圧、単位は hPa
list.main.sea_level 1013.94 海上の気圧、単位は hPa
list.main.grnd_level 1000.35 地上の気圧、単位は hPa
list.main.humidity 91 湿度、単位は %
list.weather.id 500 天気状態 の ID*
list.weather.main Rain 天気状態 の グループ名*
list.weather.description light rain 天気状態*
list.weather.icon 10n 天気アイコン ID*
list.clouds.all 56 曇り度合、単位は %
list.wind.speed 8.3 風速、単位は メートル/秒
list.wind.deg 258.503 風向、北 0 の 時計回り
list.rain.3h 0.2975 過去3時間の降雨量
list.snow.3h 0.1575 過去3時間の降雪量
list.dt_txt 2017-01-27 09:00:00 データ計算時間、UTC
  • Weather condition codes に 対応表がある
    ※ API ドキュメントに Internal parameter とあるものは除く

データの利用について検討

現在の天気と予報では JSON の パラメータ名が異なるものの、おおよそで同じような内容になっています.

天気を表す部分では weather.main が 中心で、グループ名(main) もしくは 天気状態(description) や アイコンを使って天気を表す感じでしょうか.
アイコンは http://openweathermap.org/img/w/10d.png の 最後の部分 10d に JSON で 取得した値を入れることで取得できます. 昼夜で 10d10n のようにアイコンが変わるようです.

気温などは main に 入っています. ここは単位を付けるだけで簡単に使えそうです. temp_mintemp_max は 大きい都市で気温差が出るようなケースに値が変わるようです.

風、特に風向きについては北を 0° として表現するようです. 現在の天気の JSON では wind: { speed: 9.56, deg: 194.002 } と なっていたので、南南西の風 9.5 メートル/秒 ですね.

降雨量 と 降雪量 は rain.3hsnow.3h に 過去3時間のデータが入ります. 降っていない場合は JSON に パラメータがないか、3h の パラメータがない状態となります. プログラム言語によって工夫が必要かもしれません. API ドキュメントには単位が乗ってませんでしたがミリメートルと思われます. 雨と雪の両方のデータが入っているケースがあり、その場合はどんな状況なのか、ちょっと気になります.

現在の天気のデータ計算時間は dt に 入っており、Unix,UTC と なっているのですが、Unix です. dtsunrisesunset を 扱う場合は、プログラム言語の処理系に合わせての対応が必要です.



必要なものがそろっており、JSON で 簡単にアクセスできるので助かります.
あとは、このデータを使わせてもらって、必要な時に必要な情報を流せるボットを作るだけですね!