アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

Slack ログ JSON を Markdown に変換する

November 25, 2019開発npm, slack-log2md

Slack ログ JSON の Markdown 変換ツールを開発した際の知見をまとめる。

開発動機

今年から The Vivliostyle Project へ参加するようになった。このプロジェクトでは議論を GitHub Issues と Slack でおこなっているのだが、後者のログを Slack 外へも公開したいという要望が挙げられた。これに応えるため、エクスポートされた JSON ファイルを Markdown に変換して保存するツールを開発することにした。

ちょうど現在の職場でも Slack を利用しており、そのログを Redmine に保存して検索できたらいいなと考えていたので、変換ツール開発は業務にも役立ちそう。

設計方針など

Slack はシンプルに見えてけっこう多機能だ。標準のメッセージに加えてスレッドやファイル添付、外部サイト URL を展開してプレビューできたりする。これらの内、本ツールでは基本的に標準メッセージだけ対応することにした。

2019/11/25 時点の機能は以下。Markdown としてシェアが広く HTML 内包の GFM (GitHub Flavored Markdown) を想定。

  • メッセージを Markdown テーブル変換
  • メッセージ日時を UTC として mm:ss 変換
  • プロフ画像 (Gravatar) を Markdown 画像変換
  • 改行を <br> 変換
  • 絵文字記法 (:smile: など) の Unicode 文字化
  • コード ブロックを <pre> 変換
  • 引用を <blockquote> 変換
  • 添付ファイルのパーマネント リンク出力
  • Markdown ファイルを UTC 単位でグループ化、ちなみにログ JSON ファイルの名前と単位は現時点だと太平洋時間となっている
  • GitHub Wiki 向けに単一ディレクトリー内でユニークなファイル名となるように出力

実装は Node.js とした。ソース コードは TypeScript で記述。前にブログへ書いた

を踏襲している。slack-log2md は CLI と Node.js API を提供しており、後者については d.ts を npm に同梱しているので @types なしに型情報を得られるはず (手元では確認済み)。

ここからは実装に際しての工夫点や苦慮したことなどを書いてゆく。

リストかテーブルか

開発初期はメッセージをリストとして出力するつもりだった。Markdown 的にも単純で実装も簡単だ。しかし時刻、アイコン、投稿者とメッセージのレイアウトまわりで融通の効かなさが厳しい感じだったのでテーブルへ移行。

結局、ログといえど見た目は元に近づけたくなるものだし、ならば始めからそうしておけばよかった。

改行

リストの場合、行末にスペースを 2 つ入れると改行になる。しかしテーブルではこれを使えない。ではどうするのかというと、GFM なら HTML が通るので <br> を使えばよい。

これはコード ブロックの変換結果となる <pre> 上でも有効なので、改行コードはすべて <br> とすることにした。

UTC

Slack のログ JSON ファイルは以下のようになっている。

  1. メッセージ時刻は "ts": "1570212554.014000"
  2. ファイル名は太平洋時間に基づく YYYY-MM-DD.json

メッセージ時刻を JavaScript の Date に変換したい場合は以下のように処理すればよい。

const ts = "1570212554.014000";
const date = new Date(Number(ts) * 1000);

しかしこれで得られた date に対してそのまま getMinutes() などを呼び出すと、実行している環境のタイム ゾーンになる。文字列化された時刻を含むテストが Travis CI 上でコケたことでこれに気づいた。私の環境は日本標準時だが、Travis CI 環境は太平洋時間のようだ。

変換処理が特定のロケールに依存するのは好ましくないので、今回は UTC とした。将来、パラメーターでタイムゾーンを指定可能にするかもしれないが、とりあえず標準は UTC でゆく。日時の各部位を UTC として取得する場合は DategetUTCFullYear()getUTCHours() といった専用メソッドがあるので、それを利用すればよい。

しかし時刻こそ UTC にしたが、ログ JSON ファイル名の日時は太平洋時間のままである。そのためこれを踏襲して Markdown ファイル名にすると、内部の時刻と一致せず混乱を招く。そのため変換処理の際に UTC 日次でメッセージをグループ化してから、その単位でファイル出力するようにした。

これでファイル名と内容ともに UTC で統一される。なお現時点の実装はチャンネル単位でこれを処理するため、メモリー効率はよろしくない。日の変更を境界に、その単位だけ処理してゆくよう修正するかもしれない。

絵文字

Slack メッセージにおける絵文字は :smile: などの記法で表現される。

これをそのまま出力した場合、絵文字への変換は Markdown ビューアーに委ねられる = 処理系依存となるため、本ツール時点で変換することで依存を回避したい。これを実現するために前述のページを眺めながら変換テーブルを作ろうとした。

しかし先行例がありそうなので調べたら emoji-toolkit を見つけたので採用。この npm は :smile: などの記法と Unicode 絵文字を相互変換してくれる。よって正規表現で /\:["']?([a-zA-Z0-9_\-]+)["']?\:/g を match させて対象を渡せば手軽に絵文字を得られるのだが、問題もあった。

Vivliostyle Slack では Kiara Translation という Bot で日英対訳しているのだが、これの出力する言語を示す国旗は :flag-gb: のように - 区切りとなっている。emoji-toolkit のテーブルではこれを _ にする必要があった。

これでおおむね絵文字を変換できるようになったが、まだ漏れるものもある。これについて個別対応するのはコスト的に厳しい。そのため命名に法則性があり一括処理できそうなら、という条件付きで対応することにした。

emoji-toolkit と TypeScript に関する補足。この npm は現時点だと残念ながら d.ts@types/emoji-toolkit は提供されていない。そうした場合、自前で src/@types/index.d.ts を定義したり declare module でお茶を濁すものだけど、もっと雑にやる方法として今回は typeas によるダウン キャストを試してみた。こんな感じで書く。

export type Emoji = {
  shortnameToImage: (str: string) => string;
  toImage: (str: string) => string;
  toShort: (str: string) => string;
  shortnameToUnicode: (str: string) => string;
};

const emoji = require("emoji-toolkit") as Emoji;
export default emoji;

これを d.ts ではなく普通の ts ファイルとして保存。どうせ any を私家版ダウン キャストする時点で危険なのだから、declare よりも記述の楽な type で十分じゃないか?と考えての対応である。

プロフ画像

Slack メッセージ上に表示されるユーザーのプロフ画像は Gravatar にホストされるようだ。WordPress や Redmine は Gravatar を利用しているので、Slack のそれも外部で表示可能だと考えてよいだろう。

ログ上には Gravatar への画像 URL をサイズ単位で複数定義している。それらの内、今回は Slack 上の表示に近い 72x72 を採用することにした。

添付ファイル

プロフ画像と異なりメッセージへの添付ファイルは Slack がホストしている。そのため Slack へのログインなしには得られない。またファイルも公開範囲が Slack だと想定しての添付だろうから、ツールでダウンロードするのは好ましくないだろう。例えば記念写真とか。

この点について Vivliostyle 村上さんと相談したら、ログには添付ファイルのパーマネント リンクが含まれているのでこれを出力するのはどうか?と提案された。

妙案である。この方法ならリンクとして機能するし、それを見るためには Slack のログインを必須にできる。というわけでそのようにした。

引用メッセージ

引用メッセージといってもメッセージ中の > ではない。それはそれで <blockquote> 変換しているが、本項のそれは別物。

Slack には外部サイトや他のメッセージを引用する機能がある。ログ JSON 上の定義は attachments。これについては Slack 上の表示を再現するためのデータもあるのだが、真面目にやると HTML 部分が増えるのでやめた。

代わりに titletext があればそれを並べて出力する。text のほうは Markdown 変換しつつ <blockquote> へ包むことで Slack の引用っぽく見えるようにした。

がんばれば oEmbed 対応してるサイトを展開できそうだが、これは表示時点の最新を出力するものという感じなので、Slack のスレッド同様に対応は見送る。

GitHub Wiki

ここまでで主要な機能は実装したのだが、出力された Markdown を GitHub Wiki へアップロードする際に苦労した。

当初 GitHub Wiki の GUI を利用して 1 ページずつコピペしていたのだけど、ログが 100 を超えていたため 5 時間ぐらい掛かった。その後、チャンネル単位で不要なものが出たり、公開の始点にする日を変更するなどあって仕切り直すことにした。

次のアップロードまでにもっと楽できないか?と調べてみたところ、GitHub Wiki はそれ自体が Git リポジトリーとして操作可能なことを知る。

1 ページでも作成すれば、右下に "Clone this wiki locally" という欄が出現して、そこにある URL で git clone 可能となるのだ。実際に取得した後に前述の作業時の commit 履歴と公式ヘルプとあわせて読み、以下の仕様を把握した。

  • ファイル名がそのまま Wiki ページ名になる
  • GitHub Wiki 上で [[Name]] リンクで生成したページはリポジトリー上で Name.md になる
  • Wiki ページ名の空白は実ファイルで - に置き換えられる
  • すべて単一ディレクトリーに出力されるため、全ページの名前はそれぞれ一意でなくてはならない

これを踏まえて GitHub Wiki 用の出力モードを実装。例えばログが #general2019-11-25 なら slack-general-2019-11-25.md というファイル名で出力する。

またツールではオマケとして索引ページも出力しているのだが、そのファイル名は slack-general.md とし、リンクは [[2019-11-25|slack-general-2019-11-25]] のように表示名を簡素化した。これがリストとして並ぶ。

接頭辞として slack- を付けているのは既存の Wiki ページに対する衝突を避けるため。ワーク スペース名も付けたほうがより一意性を向上できるのだが、複数ワーク スペースを単一 Wiki にまとめたい要望もなかったので、それはやめておいた。

こうした処理を経て出力したものを git clone した Wiki リポジトリーへ commit、push してみたところ想定どおり Wiki ページが生成されてリンクも機能することを確認。

今後の予定

現時点では以下へ対応予定。

  • 時刻が UTC であることを明記する #32
  • 外部からのリンクに備えてメッセージに一意識別子を振る #31

もしこの記事を読まれた方で要望やバグ報告などがあれば、ぜひ Issues へ登録してください。