IGGG

群馬大学電子計算機研究会 IGGG

IGGG

IGGG

Information technology researching society

of the Gunmer,

by the Gunmer,

for the Gunmer

Persona Color Theme © - 2019 IGGG

Submodule と GitHub Pages についてイロイロとテストしてみた

2016-12-05

サムネイルを表示したい

2016-12-03

ADVENTAR の更新を通知する Slack BOT を作ってみた

2016-12-01

ジオタグ解析アプリを作りました

2016-10-19

AnaQRam を作りました

2016-10-18

GitHub Pages + Hexo + CircleCI + Heroku で自動デプロイ管理

2016-05-30

Github Pages はじめました

2016-05-29
2016-12-05

Submodule と GitHub Pages についてイロイロとテストしてみた

名古屋支部長のひげです。

これは一応、IGGG アドベントカレンダー 5日目の記事です。

今回は C91 に向けて GitHub の仕様をイロイロとテストしてみたときの話です。

いきさつ

IGGG はココ何回か彼の有名なコミケに出展しています。
とある事情より、前回(C90) は GitHub にて編集・管理を行いました。
今回(C91)も同様に GitHub で編集・管理しようと思ったのですが、部誌(Lollipop)用の Web ページを GitHub Pages で作ろうと考え、この野心との兼ね合いでイロイロと試行錯誤しました。

考えを整理すると以下の通りです。

  • C90 のときのリポジトリがある (IGGG/lollipop-vol4)
  • C91 のリポジトリを作りたい (IGGG/lollipop-vol5 ?)
  • Lopllipop 用の GitHub Pages を作りたい

考え得る案は2つ

  1. 1つのリポジトリで全てを実現
  2. 全て各々のリポジトリで実現

前者の場合は既にある lollipop-vol4 リポジトリを次のように改変することになる。

IGGG/lollipop
| - lollipop-vol4
| | - ...
| - lollipop-vol5
| | - ...
| - README.md
| ...

これは新しい巻が出るたびにクローンせずに済むので有り難い。
しかし、古い巻に関係ない人も全ての巻のデータを落とさないといけなくなる(例えば vol.5 は寄稿するが vol.4 では寄稿しなかった人)。
これは重い気がする。

なので、後者を採用してひと工夫することにした。
それが Submodule である。

Git Submodule

詳しくはググってください。

このあたりを読むとわかりやすい。

端的に言うと、Git Submodule は、リポジトリの下にリポジトリを追加する方法である。
自分的に一癖ある機能で、未だによくわかっていない。

最終的なリポジトリ構成

次のようなリポジトリ構成にした

  • lollipop-vol4 : vol.4 のプライベートリポジトリ
  • lollipop-vol5 : vol.5 のプライベートリポジトリ
  • lollipop : lollipop 全体の管理用パブリックリポジトリ
    • submodule として lollipop-vol4 と lollipop-vol5 のリポジトリを持つ

コレ

つまり、パブリックリポジトリの中にプライベートリポジトリを Submodule として持つことになる。

本題

で、パブリックリポジトリがプライベートリポジトリの Submodule を持つ場合

  • そもそも可能か?
  • GitHub Pages の挙動はどうなるか
    • docs 以下に置いた場合
    • gh-pages ブランチに置いた場合

という疑問が沸いたので検証してみた。
という話。

検証

検証用に利用したリポジトリはコチラ 。

パブリックリポジトリにプライベートリポジトリの Submodule を持たせる

できた。

ただし、プライベートリポジトリにリード権限が無い人が Submodule にアクセスしようとしても、エラーが返ってくる(そりゃそうか)。

クリック!

404エラー...

docs 以下に GitHub Pages を設定

最近のアップデートで、GitHub Pages の index.html をプロジェクトページであれば master ブランチの docs に置いても良くなった。
その方が、無駄にブランチを分ける必要が無いので便利な場合がある。

今回も可能であればそうしようと思ったのだが…

どうやら、プライベートリポジトリの Submodule を持つビルドできないようだ。

怒られた

メールまで飛んでくる始末である。

gh-pages 以下に GitHub Pages を設定

うまくいった。

まぁそりゃそうか。

コレとココ

GitHub Pages はリポジトリ内を漁れるので注意。

まとめ

  • パブリックリポジトリにプライベートリポジトリの Submodule を持たせれる
  • プライベートリポジトリの Submodule を持つリポジトリを GitHub Pages のソース置き場にできない
    • プライベートリポジトリの Submodule が無いブランチ(gh-pages)であれば可能

これ利用してできたのがこのページ 。

おしまい

Web
GitHub
2016-12-03

サムネイルを表示したい

IGGG 名古屋支部のひげです。
(そろそろ、他の人にも書いてほしいなーと思ってます。)

たまにはラフな話題を。

かなしい

現在、IGGG アドベントカレンダーをやっていて、このサイトをリンクさせたりしてたのですが…

サムネがうまく表示されない

結局

今使ってるテーマこのテーマには cover というパラメータがあって、それを設定してあげればよかっただけ…

---
title: サムネイルを表示したい
date: 2016-12-03 15:55:01
tags:
- JavaScript
categories: Web
cover: "/images/vs-thumbnail/before_after.png"
photos: "/images/vs-thumbnail/before_after.png"
---

デフォルトの設定がされてなかったのでエラーが出てた。
なので、_config.yaml に cover: /IGGG_l.png を追加した。

試行錯誤

こっからは無駄な試行錯誤…

サムネイル画像はそもそもどーやって決めてるのか分らなかったので、去年のカレンダーを見て考えた。

なんとなく、一番最初の img タグをチェックしてるみたいだった(ホントはそれだけでは無かった)。

で、このサイトの最初の img タグの中身が空 <img s1c=""> なのが問題なのかと思って(違う)、これを修正した。

VS. 空の img タグ

この原因はココ

<article class="<%= item.layout %>">
<% if (item.photos){ %>
<%- partial('post/gallery') %>
<% } %>
<div class="post-content">

本来、photos というパラメータが設定されてなければ書かれないはずなのだが、なぜか if 以下が実行される。
おそらく、デフォルトで食う文字が設定されている。

どこで設定されてるかは分らなかったので愚直に、

<article class="<%= item.layout %>">
<% if (item.photos && item.photos != ""){ %>
<%- partial('post/gallery') %>
<% } %>
<div class="post-content">

とした。

が直らない (そりゃそう)

ちなみに、このテーマは photos パラメータを設定すると、この記事の冒頭みたいな画像ギャラリーが出てくる。
知らなんだ。

VS. 相対パス

きっと原因は画像のパスが相対パスに違いない!と思って(違う)、絶対パスの img タグを HTML ファイルの冒頭に埋め込むことにした。

_config.yaml に url として書いてあるのでそれと、thumbnail というパラメータを加えて

<div class="thumbnail">
<img src="<%- config.url %><%- item.thumbnail %>">
</div>

と頭に書いてみた。
CSS で thumbnail クラスの display パラメタを none にすれば画像は出てこない。

うまく、HTMLは埋め込めた。

が直らない (そりゃそう)

急がば回れ

手詰まりになったので、 ちゃんと エラーのところを見ることにした(最初からそうしろ)。

https://iggg.github.io/2016/12/01/adventar-slack-bot/undefined が無いと怒られている…

undefined … ?

HTML ソースコードを見ると

<meta property="og:image" content="undefined" />

おまえかー

ということで、これを設定しているコードを調べてみると

<% if(page.cover) { %>
<meta property="og:image" content="<%= page.cover %>" />
<% } else { %>
<meta property="og:image" content="<%= config.cover %>"/>
<% } %>

あー、cover というパラメータがあるのね…
undefined が返ってたのは _config.yaml に cover が設定してなかったせいか…

前述したとおり、_config.yaml と記事の cover パラメータをちゃんと設定したら表示された。

おしまい

こんなしょーもない事に4時間もかけてしまった…orz

Web
JavaScript, HTML
2016-12-01

ADVENTAR の更新を通知する Slack BOT を作ってみた

IGGG 名古屋支部のひげです。

今年もとりあえずやってみた 群馬大学電子計算機研究会 IGGG Advent Calendar 2016 1日目の記事です。

1日目ということで、Advent Calendar に準じたネタを。

いきさつ

去年、なんとなくやりはじめた Advent Calendar 。

その時の話。
とりあえずググってみた結果、まぁ、Qiita が人気ですよね。

Qiitaが人気ですよね

ここ に簡単にまとめてある。

便利ではあるのだが、部活用の身内カレンダー(身内意外に書かれてもいいけど)を Qiita でやるのは憚られる。
で、次に使われてそうなのが ADVENTAR 。

サークルとかでも使われた事例はある。

サークルで使われてるのはADVENTAR

で、問題はここから。

Qiita は RSS による通知機能がある。
なので、これを利用して、記事の追加や更新を Slack 等へと簡単に飛ばせる。
しかし、ADVENTAR にはない 。

ADVENTAR にはない

ということで、スクレイピングして飛ばすことにした。

あった。

slack bot スクレイピング で検索

Google Apps Script

画像の通り、一番上にヒットしたのが Google Apps Script (GAS) を用いたモノだった。

これは、JavaScript like な言語で書いたスクリプトを Google Drive に置いておくことで実行できるというモノ。
定期的に自動実行させたり、Webフックして実行したり、Google Apps を拡張したり、イロイロ使える。
なにより、タダで使えるのがうれしい。

まぁ詳しくはググってみてださい。

Goal

今回の目的のために、作る GAS プログラムを3つのステップに分ける。

  1. ADVENTAR のサイトをWebスクレイピングして参加情報を取得くる
  2. DBの代わりにスプレッドシートへ情報を保存・参照
  3. 前の情報との差分を取って更新情報を Slack に送信

つまり、

  1. GAS によるWebスクレイピング
  2. GAS によるスプレッドシートの操作
  3. GAS による Slack へのメッセージ送受信

を実現すればよい。

最終的なコードはコチラ

0. GAS の準備

まずは GAS の準備から。

GASは、スプレッドシートやドキュメントと異なり、デフォルトではインストールされていません。
なので、機能を追加する必要があります。

Google Drive で 右クリック し、一番下の その他 から アプリを追加 をクリックします。

そしたら、google apps script を検索してインストール(接続)。

google apps script を検索

あとは、スプレッドシートとかと同じように、Drive 内に作成できる。

専用のディレクトリを作成して、

  • Bot 用の GAS ファイル
  • DB 用のスプレッドシート
  • Bot 用のアイコン画像

を置いておいてください。

1. GAS によるWebスクレイピング

まぁありますよね

まぁ出てきますよね。

適当に参考にしながら作った。

準備

最初は以下のサイトを参考にしながら DOM Tree っぽく処理しようとしたのだが、XmlService.parse という関数は正しい形式の HTML でないとパース出来ない 。

  • 参考 : [GAS]HTML/XMLをパースする - 技術のメモ帳

よって、こっちの愚直にパースする方法をとることにした。

  • 参考 : Google Apps ScriptでスクレイピングしてSlackに定期ポストするbotを瞬殺で作った - Qiita

Parser という外部ライブラリを用いる。

GAS に新しく外部ライブラリを追加するためには、以下の手順を行う必要がある。

  1. GAS ファイルを開いてツールバーの リソース をクリック
  2. ライブラリ をクリック
  3. ライブラリを検索 にキーを入力して追加

Parser のキーは M1lugvAXKKtUxn_vdAG9JZleS6DrsjUUV 。
バージョンは新しいのを選べばいいと思う(今回は 7 を使った)。

テスト

例えば次のコードを書いて実行し、ログで確認。

function postMessage() {
var url = 'http://www.adventar.org/calendars/1137'
var html = UrlFetchApp.fetch(url).getContentText();
var doc = Parser.data(html)
.from('<div class="mod-calendarHeader"')
.to('</div>')
.build()
Logger.log(doc);
}

ログはツールバーの 表示 から ログ をクリック。

[16-11-08 21:42:01:217 JST]  style="background: #8AC5D2"><div>
<h2>群馬大学電子計算機研究会 IGGG Advent Calendar 2015</h2>

<div class="mod-calendarHeader-meta">
<p>作成者:<a href="/users/8528" class="mod-userLink"><img s1c="http://www.gravatar.com/avatar/6491e85d52916cfb063372cec9edb6cc?size=23&amp;d=mm" width="23" height="23" class="mod-userIcon" /><span>noob</span></a></p>
<p>登録状況:25/25人</p>

もちろん、頭から探索しているだけなので、括弧やHTMLタグの対応を取ってはくれない。
つまり、同じタグが入れ子になっていると、最初の方の閉じタグを取ってきてしまう。
欲しい情報を望んだとおりに取得するためには工夫が必要だ。

ADVENTAR の場合

  • 参考: Adventerの日記をSlackに流してみよう一人プロジェクト - hotchpotch

なるほど、下の方を取ればいいのか。

適当に書いてから共通部分を関数化して、最終的に次のようになった。

function doPost(e) {

const YEAR = '2016';
const URL = 'http://www.adventar.org/calendars/1572';

/* Scraping */
var html = UrlFetchApp.fetch(URL).getContentText();
var table = parseByTagAndClassId(html, 'table', 'mod-entryList');
// Entry is [date, user name, comment, title, url]
var entries = Parser.data(table)
.from('<tr class="" id="list-')
.to('</tr>')
.iterate()
.map(function(entry){ return parseEntry(entry, YEAR); });
}

function parseEntry(entry, year) {
var date = year + '/' + parseByTagAndClassId(entry, 'th', 'mod-entryList-date');
var user = parseByTag(parseByTagAndClassId(entry, 'a', 'mod-userLink'), 'span');
var comment = parseByTagAndClassId(entry, 'div', 'mod-entryList-comment');
var title = parseByTagAndClassId(entry, 'div', 'mod-entryList-title');
var url = parseByTag(parseByTagAndClassId(entry, 'div', 'mod-entryList-url'), 'a');

return [date,user,comment,title,url];
}

function parseByTagAndClassId(data, tag, classId) {
var temp = Parser.data(data)
.from('<' + tag + ' class="' + classId + '"')
.to('</' + tag + '>')
.build();
return temp.substring(temp.indexOf('>') + 1, temp.length);
}

function parseByTag(data, tag) {
var temp = Parser.data(data)
.from('<' + tag)
.to('</' + tag + '>')
.build();
return temp.substring(temp.indexOf('>') + 1, temp.length);
}

この Parser ライブラリでは、たぶん、行末までパースというのができないので、temp.substring(temp.indexOf('>') + 1, temp.length); という感じに自分で書いた。

なんで、日付に年を加えてるのかと言うと、スプレッドシートに書き込むときに書いておかないと、今年の年を勝手に書き込むからだ。

2. GAS によるスプレッドシートの操作

次に、スプレッドシートをDB代わりとして操作する。

  • 参考:Google Apps Scriptのスプレッドシート読み書きを格段に高速化をする方法

と他にも Google の公式ドキュメントを参考にした。

Properties

実際にいじる前に必要な知識を一つ。

野生のコードを眺めてると、けっこう以下の行を見かける。

var prop = PropertiesService.getScriptProperties().getProperties();

これは所謂、環境変数みたいのをとってきている。
なんらかのパスワードや ID を直接コードに書いておくのは望ましくないので、システムの中に書いておくのである。

正直はじめ、どーやって設定するかわからなかったがやっと見つけた。

ツールバーの ファイル から一番下の プロジェクトのプロパティ をクリック。
今回はスクリプト単位で設定したいので、スクリプトのプロパティに行を追加していく。

スプレッドシートの準備

予めスプレッドシートを作っておく。
スプレッドシートの名前は何でも良い。
シート名は 2015 とか 2016 などの 年 にする。

スプレッドシートの読み取り

読み取るためにはスプレッドシートの ID が必要だ。

開いたときのURL docs.google.com/spreadsheets/d/XXXXX/edit#gid=0 の XXXXX という部分だ。
直書きしても良いが、前述した Properties に追加しておこう。

前のコードの doPost 関数を以下のように拡張する。

function doPost(e) {

/* Scraping */
// ...

const DAYS = [ '12/01', '12/02', '12/03', '12/04', '12/05'
, '12/06', '12/07', '12/08', '12/09', '12/10'
, '12/11', '12/12', '12/13', '12/14', '12/15'
, '12/16', '12/17', '12/18', '12/19', '12/20'
, '12/21', '12/22', '12/23', '12/24', '12/25'
];
const COLUMN_NUM = 5;

var prop = PropertiesService.getScriptProperties().getProperties();

/* Load Spread Sheet */
var sheet = SpreadsheetApp.openById(prop.SPREAD_SHEET_ID).getSheetByName(YEAR);
var oldEntries = sheet.getRange(1, 1, DAYS.length, COLUMN_NUM).getValues();
}

イロイロ考えた結果、日付を列挙しておいた方が楽だった。
getRange(a,b,c,d).getValues() で (a,b) から (c,d) までの範囲を2次元配列として取得する。

直接アクセスする方法もあるが、必要な分だけ予め配列として読み取って、JavaScript として処理した方が速いらしい。
なのでそうしてる。

スプレッドシートの更新

スクレイピングして得た情報 entries から新しくスプレッドシートに書き込むデータを作成して、書き込む。

function doPost(e) {

/* Scraping */
// ...

/* Load Spread Sheet */
// ...

/* Update Spread Sheet */
var newEntries = DAYS.map(function(d) { return [YEAR + '/' + d,'','','','']; });
entries.map(
function(entry){
newEntries[getIndexByDate(newEntries, entry[0])] = entry;
});
sheet.getRange(1, 1, DAYS.length, COLUMN_NUM).setValues(newEntries);
}

function getIndexByDate(entries, date) {
for (var i = 0; i < entries.length; i++) {
if (entries[i][0] == date)
return i;
}
return null;
}

見ての通り、getRange(a,b,c,d).setValues() で書き込んでいる。

3. GAS による Slack へのメッセージ送受信

最後にいよいよ Slack に Bot としてメッセージを飛ばす。

  • 参考 : 非エンジニアがカップル専用アプリ「Slack」でGAS製Bot運用してみた - Webを楽しもう「リパレード」

GAS 側にタイマーを仕掛けて、一日一回とってくるのも良いが、おそらく12月に入るまで更新は少ないだろうから、Slack の Advent Calendar チャネルで特定のキーワードを打ったら返ってくるようにする。

Slack の準備

Outgoing WebHooks というインテグレーションを追加する。

ココにアクセスして Outgoing WebHooks と検索して追加。

設定項目のうち、重要なのは以下の4つ。

  • Channel
  • Trigger Word(s)
  • URL(s)
  • Token

Channel で指定したチャネルで、 Trigger Word(s) で指定したワードから始まるメッセージを送信すると、URL(s) で指定したプログラムが動く、と言う感じ。
Token は認証に使うので、VERIFY_TOKEN として GAS の Properties に追加しておく。

まぁ、認証は無くても良いが、GAS コードのURLが漏れると、実行されまくるので注意。

Slack API for GAS

作ってくれてた、ありがたい。

  • 参考 : Slack BotをGASでいい感じで書くためのライブラリを作った - Qiita

Parser ライブラリのときと同じように追加する。
キーは M3W5Ut3Q39AaIwLquryEPMwV62A3znfOO 。

Slack の API を使うには専用のトークンが必要なので、ココにアクセスして発行してもらう。
下の方にある Generate test tokens をクリックする。

生成されたトークンは SLACK_API_TOKEN として Properties に追加しておく。

画像を利用

最初の方に用意した Bot 用のアイコンを利用するにはひと工夫が必要である。

  • 参考 : Google Drive に保存した画像を直接呼び出せるURLの取得 - Qiita

ドライブ中で画像を選択し、ツールバーの鎖のようなマークをクリックし、共有可能なリンクを取得する。
すると、drive.google.com/open?id={id}のようなフォーマットのURLを得るはずだ。

Webサイトなんかに埋め込むためには、このURLを drive.google.com/uc?export=view&id={id} のように書き換えて使う。

なので、この {id} を ICON_ID として Properties に追加しておく。

GAS コード

以下のように拡張する。

function doPost(e) {

var prop = PropertiesService.getScriptProperties().getProperties();
const BOT_NAME = 'Gunmer';
const BOT_ICON = 'http://drive.google.com/uc?export=view&id=' + prop.ICON_ID;

if (prop.VERIFY_TOKEN != e.parameter.token) {
throw new Error("invalid token.");
}

/* Scraping */
// ...

/* Load Spread Sheet */
// ...

/* Update Spread Sheet */
// ...

/* Post Message to Slack */
var slackApp = SlackApp.create(prop.SLACK_API_TOKEN);
var channelId = slackApp.channelsList().channels[0].id;
var option = { username : BOT_NAME, icon_url : BOT_ICON };

var noUpdate = true;
for(var i = 0; i < newEntries.length; i++) {
var text = null;
switch(diffEntry(newEntries[i], oldEntries[i])) {
case 'updated':
text = '更新がありました!\n' + makeMessage(newEntries[i]);
break;
case 'added_entry':
text = '新しい記事です!\n' + makeMessage(newEntries[i]);
break;
case 'deleted_entry':
var text = 'キャンセルがありました...\n'
+ newEntries[i][0] +' の記事です';
break;
}
if (text != null) {
slackApp.postMessage(prop.CHANNEL_ID, text, option);
noUpdate = false;
}
}
if (noUpdate)
slackApp.postMessage(prop.CHANNEL_ID, "更新はありません", option);
}

function makeMessage(entry) {
var title = entry[3];
if (title == '')
title = 'link this!';

var message = entry[0] + ' : @' + entry[1] + '\n'
+ entry[2] + '\n';

var url = entry[4];
if (url != '')
message = message + '<' + url + '|' + title + '>' ;
return message;
}

function diffEntry(newEntry, oldEntry) {
var equality = true;
for (var i = 1; i < newEntry.length; i++)
equality = equality && newEntry[i] == oldEntry[i];

if (equality)
return 'no_update';
if (isEntry(newEntry) && isEntry(oldEntry))
return 'updated';
if (isEntry(newEntry))
return 'added_entry';
if (isEntry(oldEntry))
return 'deleted_entry';

return "undefined";
}

function isEntry(entry) {
return !(entry[1] == '' || entry[1] == undefined || entry[1] == null);
}

順に説明する。

認証

if (prop.VERIFY_TOKEN != e.parameter.token) {
throw new Error("invalid token.");
}

は言わずもがな前述した認証を行っている。
これで、自分たちの Slack からしか実行できない。

メッセージの送信

var noUpdate = true;
for(var i = 0; i < newEntries.length; i++) {
var text = null;
switch(diffEntry(newEntries[i], oldEntries[i])) {
/* ... */
}
if (text != null) {
slackApp.postMessage(prop.CHANNEL_ID, text, option);
noUpdate = false;
}
}
if (noUpdate)
slackApp.postMessage(prop.CHANNEL_ID, "更新はありません", option);

で日付ごとに前との差分を取って、更新があればメッセージを送信している。
なにも更新が無ければ、最後に 更新はありません というメッセージを送信している。

差分をとる

function diffEntry(newEntry, oldEntry) {
var equality = true;
for (var i = 1; i < newEntry.length; i++)
equality = equality && newEntry[i] == oldEntry[i];

if (equality)
return 'no_update';
if (isEntry(newEntry) && isEntry(oldEntry))
return 'updated';
if (isEntry(newEntry))
return 'added_entry';
if (isEntry(oldEntry))
return 'deleted_entry';

return "undefined";
}

で、差分をとっている。
引数はどちらも、要素数を5と仮定した配列で、同じ日付のエントリに関する新旧情報の行を想定している。

for 文を見ると、1から始まっている。
つまり、日付では比較していない。
理由は、スプレッドシートから読み込んだ旧情報の日付は Date 型として保存されており、文字列ではないので、比較できない(もとい必ず false が返る)。
なので、日付は同じと仮定して、比較している。

もし、

  • 更新が無ければ no_update という文字列を、
  • 何らかの更新はある場合は updated
  • 記事が新しく追加された場合は added_entry を、
  • 登録がキャンセルされている場合は deleted_entry を、
  • それ以外の場合は (ないけど) undefined

という文字列を返す。

数値でもよかったが可読性優先して文字列にした。

メッセージの作成

function makeMessage(entry) {
var title = entry[3];
if (title == '')
title = 'link this!';

var message = entry[0] + ' : @' + entry[1] + '\n'
+ entry[2] + '\n';

var url = entry[4];
if (url != '')
message = message + '<' + url + '|' + title + '>' ;
return message;
}

[日付, ユーザー名, 記事に関するコメント, 記事のタイトル, 記事のURL] の配列を受け取って文字列を返している。
この文字列が Slack へ送信される。

こんなメッセージを想定している。

2016/12/01 : @noob
ADVENTAR の更新を Slack に通知させる Bot の作成

ユーザー名に @ を付けてるのは、同一の slack でのユーザー名であればリンクが付くかと期待したからだ。
結局付かなかったので、わざわざ @ を付ける必要はないです。

message = message + '<' + url + '|' + title + '>' ; でただURLを貼るのではなく、記事のタイトルにハイパーリンクを付けている。
ただし、記事によってはタイトルが無い場合があるので、2~4行目あたりで、タイトルがない場合は link this! という文字列を代用している。

URLを指定する。

Slack の準備 のとこで説明した、URL(s) に指定するURLを取得する。

GAS のツールバーの 公開 から ウェブアプリケーションとして導入 をクリック。

ここで、アプリケーションにアクセスできるユーザー を 全員(匿名ユーザーを含む) にする必要がある。

導入 を押せば、URLが発行されるので、それを Slack のインテグレーションの Outgoing WebHooks の URL(s) にコピペする。

4. コードの更新

最後に注意点。

コードを 更新するたびに必要かはわからないが、 (必要でした) 更新してもうまく実行されない場合は、もう一度、上記の手順で
ウェブアプリケーションとして導入 をし、プロジェクトのバージョン をあげること。

ちなみに、仮にコードを更新するたびにバージョンをあげないといけないならば、ADVENTAR のURLや年が変わるたびに、あげないといけなくなる。
なので、最終的にはそれらを Properties にした。

最終的に

こんなかんじ

ひとりさみしく

ついでに

定期ポストしたい場合の手っ取り場合方法は、Bot をフックするためのメッセージを定期ポストする GAS コードの Slack Bot を作るのがよさそう。

コードはサクッとこんな感じ

function postMessage() {

var prop = PropertiesService.getScriptProperties().getProperties();

const BOT_NAME = 'Gunmer BOT';
const BOT_ICON = 'http://drive.google.com/uc?export=view&id=' + prop.ICON_ID;

/* Post Message to Slack */
var slackApp = SlackApp.create(prop.SLACK_API_TOKEN);
var option = { username : BOT_NAME, icon_url : BOT_ICON };

slackApp.postMessage(prop.CHANNEL_ID, prop.MESSAGE, option);
}

CHANNEL_ID は #randome とかで良い。
後は、GAS の設定で定期ポストをするだけ。

半日置きに定期ポストされるはず

Bot から Bot へ

追記

まかせろ

ほい

できたぜ

おしまい

Web
Slack, Bot, Google Apps Script
2016-10-19

ジオタグ解析アプリを作りました

IGGG 名古屋支部 支部長の ひげ です。
前回に続いて今回も群大理工学部の学園祭 群桐祭 のイベント、テクノドリームツアー用に作成した、ジオタグ解析アプリ、Where-is-This (名前は適当)について紹介したいと思います。

Where-is-This とは

画像を与えると、画像の位置情報を解析して、Google Map にマッピングした情報が返ってくるというモノです。

送信前(左)と送信後(右)

このプログラム自体は IGGG の期待の新星 atpons くんが10分くらいでこしらえてくれたものです(はやい)。
それを少しだけ修正して IGGG の Heroku にあげました。

ココ でアクセスできますが、無料枠なんでアクセスが集中すると止まると思います。

コンセプトとしては、来場者に画像には位置情報が含まれているということを知ってもらおうというモノでした。
画像には位置情報が含まれていて、撮った場所を特定することできる。
なので、外にあげるときは気を付けよう、という感じです(まぁ最近のSNSは位置情報の部分を消されちゃうらしいですが)。

実はこのアプリ、結局のところ本番では、運用する余裕が無くて(テクノってすごく忙しいんです)、用意はしたのですが、使いませんでした…(ごめんね atpons)

(いいわけではないですけど、ジオタグがどーのこーのと説明する時間が無くて)

実装

別に私が作ったわけじゃないけど、ドヤ顔で紹介します。

Ruby で書かれていて、なんとたったの15行しかない!
流石 Ruby ですねぇ。

require "sinatra"
require "exifr"

get "/" do
erb :index
end

post "/upload" do
if params[:file]
file = EXIFR::JPEG.new(params[:file][:tempfile].path)
@latitude = file.gps.latitude
@longitude = file.gps.longitude
end
erb :upload
end

Sinatra

2つのライブラリを使っています。
その一つが Sinatra です。
(公式サイト曰く) Sinatra は

Sinatraは最小の労力でRubyによるWebアプリケーションを手早く作るためのDSL

だそうです。
Model View Controller(MVC)に基づかない設計で作成されており、小さく、柔軟性があるプログラミングが可能となるよう意識されている、そうです(wikipedia より)。

正直、Sinatra の仕組みについてちっっっとも知りませんが、なんとなく読める通りに動くのでしょう(適当)。

流石 Ruby ですねぇ。

Exifr

もう一つが Exifr です。
察しの付く通り、Exif Reader ですね。
位置情報を含む画像の Exif をすごく簡単に取得するためのライブラリ群です。

上のコードの10-12行目が exifr に相当します。
正直、こっちも仕組みについてはちっっっとも知りませんが、なんとなく読める通りに動くのでしょう(適当)。

流石 Ruby ですねぇ(それしか言ってない)。

修正

Heroku にあげるために以下の点を修正しました。

  • 次のように書かれた config.ru を作成
    • atpons のはローカルで動かすまでだったので無かった。
    • コレがないと Heroku とかではデプロイしても動かない(当たり前?)
require 'bundler'
Bundler.require

require './app'
run Sinatra::Application
  • Google Map の URL を HTTPs に変更
    • Heroku の URL が HTTPs なので、変更しないと動かないみたい

その他

Heroku へのデプロイはググれば出てくる一般的な方法(こういうのとか)でできました。
ただ、Heroku のリモートリポジトリと、GitHub のリモートリポジトリで、そこら辺の知識なくて、ごっちゃんになり苦労しました。

おわりに

正直、これ以上書くことないです。
すごく簡単なんで。

流石 Ruby ですねぇ。

Application
Heroku, Ruby, guntohfes
2016-10-18

AnaQRam を作りました

IGGG 名古屋支部 支部長の ひげ です。
今回は群大理工学部の学園祭 群桐祭 のイベント、テクノドリームツアー用に作成した、AnaQRam という Android アプリについて紹介したいと思います。

AnaQRam とは

宝探しとパズルをコンセプトにした、すごく簡易的なゲームです。
テクノドリームツアーは小学校低学年前後を対象にした科学体験イベントであり、それ用に作成したので、ゲーマーにはかなり退屈なゲームです。

遊び方は簡単で、まずQRコードをスキャンしまくって、パズルのピースとなる文字を集めます。
文字を見つけると ? が対応する文字に変わります。

文字を集めている

文字を集めきったら次はパズルです。
集めた文字を並び替えて正しい文字列を作ります(アナグラム)。

文字を並び替えている

ゲームとしてはこんな感じです。

このQRコードを色んな所に隠せば、結構面白いかなぁと考えて作りました。

(本番ではスペース的に隠す余裕無くて、全部壁一面に貼られてしまったが…)

実装

他の IGGG メンバもコードを読めるように(誰も読んでないだろうけど)、ベーシックに Java と Android Studio を使って作成しました。

コードは全て、私のリポジトリ に公開してあります。

全体的に、WEB上を検索して得た機能をどんどんマッシュアップしていった感じです。

(そもそも、自分で一から Android アプリを作ったのは始めて)

端末の環境

端末は群大情報科で借りた、Nexus7 を使うのですが、Android のバージョンが 5.1.1 (Lollipop) だったので、それに合わせて開発しました。

(そのため Java8 が使えない…)

また、Google Play 開発サービスの Mobile Vision API のQRコードを読み取るライブラリを使ったため、開発サービスのバージョンを 7.8 以上にする必要があります。

(このため、前日に端末の開発サービスのバージョンを全て挙げる作業が…)

他にも日本語入力するのに Google 日本語キーボードや端末の言語を日本語にする必要があります( ゲーム自体は日本語でなくても動作するので、英語のみでアナグラムをするのであれば必要ない 伏字の ? が全角なのでダメだ、あとで直します…)。

開発環境

  • Windows 10 Home
  • Android SDK Build-tools 24.0.2
    • min SDK 21
  • Android Studio 2.2

QRコードを読み取る

以下のWEBサイトを参考にしました。

  • Reading QR Codes Using the Mobile Vision API

上述したとおり、Google の Mobile Vision API を利用しています。

QRコードを読み取る他の方法として、サードパーティ製の ZXing というライブラリもありましたが、なんとなく純正のやつを使ってみました。

記事の通りに書いていったらうまく動きました。
特に難しいことしておらず、SurfaceView にカメラを貼り付けて、読み取った画像を API で解析し、見つけた時の動作を後から与えているだけです。
API の仕組み等までは流石に知らないので説明しません。

精度はかなり良く、画面に複数のQRコードが写っていても、すぐに全部読み取ってくれます。
QRコードは小さくても良く、ピントさへ合えば問題なさげです。

ただ、API のせいか、実機デバッグに使っていた私の端末のせいかわからないのですが、使ってると時々カメラが真っ暗になって映らなくなってしまいます…(再起動すればまた映りますが…)

中の仕組み

超概略図

文字は CharBox という char 型のラッパークラスを使って管理しています。
既に見つけたかどうかの flag も boolean 型で持っていて、これの真偽で toString した時の返す文字列が変わります(もちろん false のときが ?)。

class CharBox {
private char value;
private boolean flag;
private final static char defaultValue = '?';

CharBox(char c) { ... }
void setFlag() { ... }
void resetFlag() { ... }

@Override
public String toString() {
return String.valueOf(flag ? value : defaultValue);
}
}

AnaQRam の内部では答えの文字列を決めると、それを CharBox の配列へと変換します。

QRコードは実は、文字では無くただの 数字を表していて、スキャンすると対応する CharBox 配列のインデックスにアクセス して、フラグを立てます。
こうすることで、アプリ内で答えの文字列を変更しても、貼るQRコードを変更しなくても良い ようにしてます。

もちろん、インデックス以上の数字を読み取っても例外を投げて落ちたりしません。
数字以外の場合は、「QRコードが対応してないよ!」的なメッセージを出します。
数字の場合は、インデックスに使う前に、答えの文字列長で Modulo (余りを求める演算) を取ります。
そうすることで、文字列長以上の数字が来ても、問題なく扱うことができます(文字列長に合わせて貼りなおす必要がない、もちろん最大に合わせる必要はあるが)。

String displayChar(String qrText) {
try {
// 剰余を取って文字数未満の数字が出ても大丈夫にしている
int index = Integer.valueOf(qrText) % charBoxes.length;
charBoxes[index].setFlag();
return "「" + charBoxes[index] + "」をみつけた!v(≧∇≦)v";
} catch (NumberFormatException e) {
// qrText が数字以外の場合
return "ちがうQRコードだよ!(* ̄∀ ̄)\"b\" チッチッチッ";
}
}

画面への表示はタダのボタンになっています。
このボタン配列に CharBox 配列をマッピングします。
ボタンを押して入れ替えるときは、このマッピングを変えています。

正解かどうかを確かめるときは、ボタン配列から文字列を生成(つまり、伏字が混ぜっていれば り?ご となる)して、答えの文字列との equals を取ります。

その他の機能

他にも、トーストを用いたクリアメッセージ表示や、タイマー機能の追加などをしています。
参考文献は全てREADMEに書いてあるので、見てください。

困った点

そもそも Android をあまりやってなかったので、初めの方はデバッグで苦労しました。
Android Studio にはブレークポイントや逐次実行もあるので慣れてしまえばどうとでもなりますが、やっぱり普段関数型を使ってる身としては、例外ばっかのデバッグはつらいですね…

あとは、画面の回転時にビューを再構築してしまう仕様に苦労しました。
再構築してしまうため、履歴がリセット、見つけた文字が伏字に戻ってしまう。
結局、一番簡単な方法で落ち着いたのですが、その代わりにカメラが回転してくれないので、実質回転不可です。
今後直していきたいですね。

今後

個人的には気にっているので、機能を追加・修正していこうと思ってます。
ただ、単体では面白くない(QRコードがないとダメ、つまりイベントとかでしか使えない)なので、Play Store にはあげないと思います。

おわりに

端末にインストールするのにそこそこ苦労したんですが、他に用意した Unity のゲームは難なくインストールできててスゲーって思いました。
今度は Unity もいじってみたいですねぇ。

Application
guntohfes, Android, Java
2016-05-30

GitHub Pages + Hexo + CircleCI + Heroku で自動デプロイ管理

IGGG 名古屋支部 支部長の ひげ です。
今回も寂しく孤軍奮闘しております。
嘘です。
Slack 使って騒いでるんで一人ではないです。
早速プルリクエストあったし。

さて、今回は前言通り デプロイの自動化 を行いました。

実は GitHub Pages 利用する少し前に Slack 上で CI (継続的インテグレーション) について(ほんの少しだけ)話していて、
GitHub Pages の話題が上がったときに、CI の簡単な例がだよ、といつものお意見番が下記の記事を教えてくれました。

  • チームブログをGitHubとHexoではじめよう!

早速、 パクッて 参考にしてみました!

Goal

結局何をしたいのかというと

  1. GitHub に大本である source ブランチを プッシュしたら自動でデプロイ してほしい
  2. source ブランチ以外をプッシュした場合は ステージ環境に自動でデプロイ してほしい

の2点です。
前者の理由は単純に手間を省くためです。
後者の理由は、新しい記事の精査を実行環境のない人でも行えるようにです。

(まぁ本音は単純に私が面白そうだと思ったからですけど)

そのために、GitHub へのプッシュに対して自動でデプロイしてくれるサービスとステージング環境が必要です。
前述した記事を参考にして、前者には CircleCI を、後者には Heroku を利用したいと思います。

追記 (2016.6.2)
ブランチの構成を変更しました。
例の相談役さんがステージング環境へのデプロイが競合しないようにステージング用のブランチを作った方が良いと助言してくれました。
なので

  • source master へ自動デプロイされるブランチ
  • staging ステージング環境へ自動デプロイされるブランチ
  • それ以外のブランチはデプロイされない

の構成に変更しました。
変更箇所は後述します。

CircleCI

CircleCI は GitHub と連携して実行やテスト、デプロイなどを自動で行ってくれるサービスです。
このように、プログラミングに付随する様々な作業を自動化して継続的に管理する事を 継続的インテグレーション と言います(たぶん)。
私はコノコトについてちゃんと勉強してないので、後は自分で調べてください(おい)

継続的インテグレーションをサポートするサービスは他にもイロイロあります。
CircleCI の特徴については以下のスライドでも見てください(おい)

  • はじめての CircleCI

では順に準備を行っていきます。

CircleCI の準備

CircleCI は既存の GitHub と認証を行います。
今回は、私のアカウントを使う事にします。

まず、公式サイト に行きます。
後は、Sign up のところを押して、ポチポチしていくだけです(ざっくり)。

登録が完了したら、GitHub の方で認証を行います(たぶん)。
ココに行って Add to GitHub を押せばいいはずです。

これで、CircleCI のダッシュボードに GitHub のリポジトリが出てくるはずです。

出てきたら、CircleCI で管理したいリポジトリを選択します。

選択すると早速リポジトリのビルドを始めようとしますが、設定ファイルを入れてないのでコケるはずです。

Heroku

いったんハナシを脱線して Heroku について簡単に説明します。
Heroku とは AWSのIaaS上に構築されたPaaSで、Gitでデプロイできたり、Webアプリの開発から公開までがミラクルスペシャルウルトラスーパーメガトン簡単にできるプラットフォーム だそうです。
次のサイトに書いてありました。
残りは参照してください(おい)

  • Heroku導入メモ - GitHub

と言っても、上記のサイトの情報は少し古く、料金体系が結構変わって、無料枠の容量の上限が 300MB に増えていたり、無料枠では日に6時間はスリープさせないといけなかったり、になっています。
そのうえ、また無料枠を変更するみたいです。

まぁ、詳しくは公式サイトを読めばいいんじゃないかな…(おいおい)

Heroku の準備

イロイロと料金体系が変わっているみたいですが、(たぶん)ステージング環境としてならまだ使えそうなんで使っていきます。

まず、公式サイト にアクセスして、Sign Up します。
Pick your primary development language は単純によく使うプログラミングを聞いてるだけです。

次に App を作成します。
Heroku Toolkit をインストールして、下記コマンドを実行しても良いですし、ダッシュボードからでもできるはずです。

$ heroku login
$ heroku create

CircleCI と Heroku を連携させる

公式サイト の指示に従って、CircleCI から Heroku を認証させます。

CircleCI のダッシュボードでリポジトリ固有のページに移動します。
そしたら、右上の Project Settings をクリックします。
次に、左下の方にある Heroku Deployment をクリックします。

  • Step 1
    CircleCI アカウントのコノページに Heroku API Key を登録します。
    Heroku API Key は Heroku のアカウントページ の下の方にあります。
  • Strp 2
    Heroku と SSH 認証を設定します。
    とはいっても、Heroku Deployment の Step 2 のところをクリックするだけで出来てしまいます。

後は、リポジトリに circle.yml という設定ファイルを置くだけです。

circle.yml の作成

最初に紹介したサイトを参考にして、GitHub Pages 用のリポジトリに以下のような設定ファイル(circle.yml)を加えました。

machine:
timezone: Asia/Tokyo
node:
version: 4.4.5
deployment:
production:
branch: source
commands:
- git config --global user.name "IGGGorg"
- git config --global user.email "contact@iggg.org"
- git submodule init
- git submodule update
- ./node_modules/.bin/hexo clean
- ./node_modules/.bin/hexo generate
- ./node_modules/.bin/hexo deploy
staging:
branch: /.*/
commands:
- git config --global user.name "IGGGorg"
- git config --global user.email "contcat@iggg.org"
- ./node_modules/.bin/hexo clean
- ./node_modules/.bin/hexo generate
- ./node_modules/.bin/hexo deploy --branch $CIRCLE_BRANCH --config _staging_config.yml
general:
branches:
ignore:
- master
test:
override:
- echo "test"

source ブランチか、それ以外かでデプロイ内容を分けています。
但し、ページ自体のブランチである、master ブランチは無視するように下の方に書いてあります。
hexo deploy --branch $CIRCLE_BRANCH --config _staging_config.yml とすることで、デプロイの際に参照する Hexo の設定ファイルを _staging_config.yml に指定できます。

追記 (2016.6.2)
前述したとおり、ブランチ構成を変更したので、上記の circle.yml を一部書き換えました。

staging:
- branch: /.*/
+ branch: staging
commands:

追記終わり。

次に、その設定ファイル(_staging_config.yml)を加えましょう。
またもや、例のサイトを参考にして、deployment のところを次のように書き換えます。

# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
type: heroku
repo: git@heroku.com:<作成したAPP名>.git

更に、上の方の url: も書き換えておきましょう。

package.json ファイルの書き換え

最後に、package.json を書き換えます。

Hexo で Heroku にデプロイするには専用のパッケージ、hexo-deployer-heroku が必要です。
package.json の "dependencies": に書き加えましょう。

push !

後はプッシュするだけのはずです !

追記 (2016.6.2)
もし、source ブランチの自動デプロイで ssh 鍵について怒られた場合は以下のページを参考にしてください。

  • Adding read/write deployment key - CircleCI

新しい ssh 鍵を生成して、GitHub に公開鍵を、CircleCI に秘密鍵を登録すればいいはずです。
ちなみに、鍵を生成する際にパスフレーズを付けないようにと注意書きされてます。

おわりに

実は、こういうサービスいじるの初めてでして、まぁまぁてこずりました(笑)

IGGG のみんな、つかってくれるとうれしいなぁ。

Web
Heroku, GitHub, CircleCI
2016-05-29

Github Pages はじめました

2代目IGGG会長の ひげ です。
既にグンマーですらない私ですが、相も変わらず IGGG を盛り上げようと日々奮闘しています。
嘘です。
暇なだけです。

見ての通り、IGGG で Github Pages を始めることにしました。
主な用途は技術寄りな話題を書いていこうかなーっとゆるく考えています。

今回は、このページができるまでの私の奮闘を軽く書いておこうかなと思います。

Background

そもそもなんで Github Pages しようかとなったかってハナシなんですけど、

  • 新学期(既に5月末)になって、
  • IGGG もやっとこさ新メンバーが増えてきて、
  • 彼ら用に Slack の使い方をまとめたものを作ろうとなって、
  • 最初は Gist でやろうとしたんだけど、
  • 記述量が多かったので、いっそのコト Github Pages でいいんじゃないというハナシが出たので、
  • 面白そうなので 勝手にひげがやっちゃった

というハナシです。

Hexo

IGGG のアカウントはあったので、適当なページを参考にしてリポジトリを作成しました。

ゴリゴリと HTML 書くのはつらそうなので、Markdown で書いて変換してデプロイしたいですよね?

問題は変換するのに何を使うか。
一番使われてるのは Jekyll という Rails のライブラリ。
しかし、うちはどちらかというと Pythonista の方が多く、Rails なんか使った暁には刺されかねません。
しかし、Rails の環境を構築するのはめんどうなので(特にWindows)、誰でも環境構築しやすいやつがいいですよね^^
そこで、Hexo という Node.js のライブラリを使うことにしました!

Node.js のインストールは公式サイトのインストーラーなんかでおしまい。

Hexoは

$ npm install hexo

でおしまい。

とっても楽ですね^^

デザインを変更

SSH でひと悶着あったのち、なんとかデプロイ成功。

で、まずはデザイン変えたくなりますよね?

ココのテーマ一覧を眺めながら、いいのを見つけたのまでは良かった。

しかし、このテーマ(今実際に使ってるやつ)は Hexo の新しいバージョンに対応して無いせいで、コンパイルできない(コレと同じ)。
たまたま、別のテーマで同じエラーに対し Fix してるコミットログがあったので、参考にして直したところうまくいった!

このテーマ、くるくるアイコンが回るのがいいよね(小並感)

シンタックスハイライト

このテーマのシンタックスハイライトが残念だったので書き換えた。
まぁ書き換えた結果が良いかと言われると怪しんだけど…

おわりに

今後は、このサイトで、皆で、Markdown でサクッと更新していきたいですね、イロイロと。

無駄に、継続的インテグレーションとか入れて遊んでみたいなーっとか思ってる。

ちなみに、

昨日でちょうど IGGG 2周年です!
やったぜ!

Web
Node.js, GitHub
  • Android

  • Bot

  • CircleCI

  • Docker

  • GitHub

  • Google Apps Script

  • HTML

  • Heroku

  • Hugo

  • Java

  • JavaScript

  • Linux

  • Node.js

  • Python

  • Ruby

  • Scrapbox.io

  • Slack

  • Twitter

  • WordPress

  • esa

  • guntohfes

  • libnss-json

Prev