もぐてっく

人は1つ歳をとるたび、1ビットづつ大きくなれると信じてた。

mikutterコードリーディングの手引き

この度、久々にOSC京都に行けることになりました!わっほーい!

しかしながらこの2年ほど、まったくOSSな活動をせずにダジャレを量産するおっさんと化していたワタクシ。

このままだとアトリウムに着いた瞬間にておくれ達に拉致られて、京都湾(?)に沈められる!

と言う強迫観念に苛まれて夜しか寝れない体になってしまったので、
これまでのノウハウを生かしてmikutterのコードを読むときに知っといた方がいいことを纏めることにしました。

一人でもmikutterに興味を持ってソースをいじり始める人が増えますように。

前提

  • ここに書かれた内容は、8/1時点のHEADのソースコードを元にしています。バージョンでいうと3.9.2 + αです。
  • プラグインを作りたい方は、まずは"Writting mikutter plugin"を読むのが良いと思います。

reference.mikutter.hachune.net

ソースコードの入手

公式サイトからgitコマンドで取得できます。
GitHubにあるmikutterリポジトリは有志がメンテしているもので、バージョンが古い可能性があります。

git clone git://mikutter.hachune.net/mikutter.git 

mikutterのディレクトリ構成

|-- core
|   |-- boot
|   |-- lib
|   |-- miku
|   |-- mui
|   |-- plugin
|   |-- skin
|   |-- system
|-- deployment
|-- tasks
|-- test

core/boot

mikutterの起動直後に動き出す処理がまとまっています。
コマンドラインオプション解析や(GTKプラグインが無い時の)イベントループ、プラグイン読み込みなどのコードがあります。

core/lib

便利なライブラリ。mikutterのカリカリ高速チューニングはこのディレクトリが担っています。

core/miku

Ruby製のLISP処理系「MIKU」がいます。
ハマると出られなくなるので、何も見なかったことにしてそっとcd ../しましょう。

core/mui

mikutterのGUI(GTK2)のコードが格納されています。GUIを改造したいときは大体ここ。
後述のcore/plugin/gtkにはGTKを呼び出すコードはほとんどないので注意しましょう。

core/skin

GUIで使うアイコンやサウンドが格納されています。

core/system

システムメッセージをTLに表示するためのユーザー(所謂みくったーちゃん)のモデルが定義されています。

core/plugin

標準プラグインが格納されています。後述します。

development

大体のLinuxディストリビューションで使えるパッケージ「AppImage」生成用のファイルが格納されています。

tasks

国際化のための翻訳関連のファイルが置いてあります。

test

ライブラリのテスト用のコードが格納されています。

標準プラグイン

mikutterはプラグイン機構が非常に柔軟で、主要な機能もプラグインとして実装されています。
その数なんと72個!この企画を選んだことを後悔しつつ、簡単に説明していきます。

core/plugin/achievement

mikutterを使っていると突如付与される「実績」を管理するプラグイン
管理するのが仕事なので、実績の発動条件や実績メッセージは実績を授与する各プラグインに分散しています。

core/plugin/activity

アクティビティタブを司るプラグイン
「ふぁぼ」「リツイート」などの標準のアクティビティもここにまとまっています。

core/plugin/alsa

LinuxサウンドシステムALSAサウンドを再生するためのプラグイン

core/plugin/api_request_file_cache

1時間ごとにTwitterからキャッシュした使用頻度の低い画像ファイルを削除するプラグイン

core/plugin/aspectframe

アスペクト比(画面の縦横比)を考慮してボタンなどのGUI部品を配置するためのフレーム部品です!(きっぱり)
はい、次行きましょう!次!

core/plugin/bitly

メッセージ含まれるURL短縮サービスBitlyで短縮したURLを展開するプラグイン

core/plugin/bugreport

たまによくmikutterが落ちた時に、作者にバグレポートを送信するプラグイン

core/plugin/account_change

Twitterやその他のサービス」(World)のユーザーアカウントを管理するGUIと、アカウントを切り替えるGUIやコマンドを提供するプラグイン

core/plugin/command

「本文をコピー」「返信」などのmikutterの標準的なコマンドが定義されています。
また「複数のメッセージが選択されている」などのコマンドが実行できる条件も定義されています。
自作コマンドを作るときにお世話になります。

core/plugin/console

Alt + Xで呼び出せるmikutterコンソールを司るプラグインです。

core/plugin/core

なんだこれ?
起動完了後にイベントフィルタのマルチスレッド動作を許可する感じ?

core/plugin/current_world

「現在選択されているアカウント」っていう概念を表現するプラグイン

core/plugin/directmessage

Twitterやその他のサービス」(World)のダイレクトメッセージを取り扱うプラグイン

core/plugin/extract

mikutterの強力なメッセージフィルタ機能「抽出タブ」を司るプラグイン

core/plugin/file_path

UNIX絶対パスをaddressableライブラリのURIクラスに変換するユーティリティ。

core/plugin/followingcontrol

プロフィール画面にフォロワー・フォロイー関係を表示するプラグイン

core/plugin/gtk

GTK2によるGUIを司るプラグイン
メインウインドウやダイアログと言った、大物のGUI部品のコードがあります。
TLなどの中身は、前述のcore/muiを参照すること。
後はイベントループがあったりします。

core/plugin/gui

通常、GUIを持つアプリはツールキット(mikutterの場合GTK2)と密接な関係があるので、
別のツールキット(Qtとか)を適用しようとするとめっちゃ改造が必要です。

しかし、mikutterはGTK以外のツールキットを使ったGUIプラグインが出てくることを想定して、
画面の操作はこのGUIプラグイン経由で行うことになっています。

core/plugin/guide

mikutterをはじめて起動したときに始まる茶番チュートリアルを司るプラグイン
ここで使われてるTLメッセージにボタンが表示できる機能は有用なので、ぜひとも汎用化したいですね。

core/plugin/home_timeline

ホームタイムラインのGUIを担当するプラグイン

core/plugin/image_file_cache

画像ファイルをキャッシュする仕組みを提供するプラグイン

core/plugin/intent

TLのメッセージのURLを開くときの処理を行うプラグイン
マストドンのURLはブラウザじゃなくてマストドンプラグインのタブで表示したいとか。そんなことが出来る仕組みです。

core/plugin/libnotify

notify-sendコマンド経由でOSの通知ポップアップを表示するプラグイン

core/plugin/mastodon

mikutterがメインターゲットとするSNSMastodon」と通信するためのプラグイン
金具さんが作ってたWorldonっていうプラグインを取り込んだ形になります。

core/plugin/mastodon_sse_streaming

マストドンでストリームからメッセージを取ってくるためのプラグイン

core/plugin/mentions

Twitterやその他のサービス」(World)のメンション(返信)を取り扱うプラグイン

core/plugin/message_detail_view

mikutter 3.4で搭載された「詳細タブ」を司るプラグイン

core/plugin/message_favorite

同じく「詳細タブ」機能のふぁぼられたユーザー一覧部分をつかさどるプラグイン

core/plugin/message_retweet

同じく「詳細タブ」機能のリツイートされたユーザー一覧部分をつかさどるプラグイン

core/plugin/modelviewer

mikutter 3.9から導入された汎用モデルビューワのプラグイン

core/plugin/notification

ウインドウ下部のステータスバーにmikutterのおもしろ最新情報を表示するプラグイン

core/plugin/notify

設定画面の「通知」の設定項目を司るプラグイン
ふぁぼやリツイートなどのイベントで通知を発動するコードもあります。

core/plugin/openimg

画像のURLから実際の画像を得るためのプラグイン

core/plugin/photo

画像のURLから画像モデルを得るためのプラグイン

core/plugin/photo_support

画像を保持するサービスから具体的にどうやって画像を取得するかを定義するプラグイン

core/plugin/profile

「プロフィール」タブの言語ファイルのみが格納されている。
enと言いながら中身がもろ日本語なのウケる。

core/plugin/proxy

設定画面の「プロキシ」の設定項目と、Net:HTTPクラスに対するモンキーパッチ。

core/plugin/quickstep

mikutter 3.9の新機能「Quick Step」。

core/plugin/quoted_message

Twitterの引用付きツイートを表示するプラグイン

core/plugin/ratelimit

Twitterの規制に引っかかったときに通知するプラグイン

core/plugin/rest

TwitterREST APIで定期的にメッセージを取ってくるプラグイン
何らかの理由でユーザーストリームが使えない時(is now)に使われていました。

core/plugin/set_view

GUIの見た目(色、フォントなど)を司るプラグイン
設定画面の「表示」の設定項目もこのプラグインで提供しています。

core/plugin/settings

設定画面のGUIを司るプラグイン
設定画面の「基本設定」の設定項目もこのプラグインで提供しています。

core/plugin/shortcutkey

キーボードショートカットを司るプラグイン
設定画面の「ショートカットキー」の設定項目もこのプラグインで提供しています。

core/plugin/skin

GUIのアイコンや通知音を一括で変更する「スキン」機能を司るプラグイン

core/plugin/skin_setting_gtk

設定画面の「スキン」の設定項目を提供するプラグイン

ちなみにこれは拙作だったりします。
あー、ヤバいなぁ。本体の進化に取り残されてるなぁ。やっぱりとしぁさんに京都湾(?)に沈められるのでは。

core/plugin/smartthread

「会話スレッド」タブを司るプラグイン

core/plugin/sound

サウンドシステム(前述のALSAとか)と連携して音声を再生するプラグイン
設定画面の「サウンド」の設定項目も提供しています。

core/plugin/spell

mikutter 3.6から導入された、1つ以上のModelが連携する処理を定義する仕組み「Spell」を提供します。

core/plugin/streaming

Twitterが爆発的に普及した理由である「ユーザーストリーム」を司るプラグイン
今はもう動きません。

core/plugin/streaming_connection_status

Twitterのユーザーストリームの接続状況をアクティビティタブに表示するプラグイン

core/plugin/tco

Twitter専属のURL短縮サービスt.coのリンクを展開するプラグイン

core/plugin/twitter

昔流行ったTwitterとか言うマイクロブログと通信するプラグイン

core/plugin/twitter_api_keys

なんだこれ?空ファイルがあるのみ。

core/plugin/twitter_search

Twitterハッシュタグをクリックしたときに、検索機能でハッシュタグを検索するプラグイン

core/plugin/uitranslator

mikutterの多言語化サポート用のプラグイン

core/plugin/user_detail_view

「プロフィール」タブを司るプラグイン

core/plugin/web

クリックしたURLを外部ブラウザで開くためのプラグイン

core/plugin/world

mikutter 3.6で導入された「Twitterやその他のサービス」を表現する「World」を司るプラグイン

mikutterに特化したRuby Gemたち

mikutterを読み解くには、mikutterに特化したいくつかのgemも読み解かないといけないかも知れません。
これらは元々はmikutterに内蔵されていた処理ですが、その汎用性の高さからgemとして切り出した方が世のため人のためになると言う判断なんだと思います。
他のgemと違ってWebに使用例が転がっている状況ではないので、mikutterでの使用箇所をExampleだと思って頑張ってください。

diva

様々なメンバを保持する「モデル」を定義するためのライブラリ。

delayer

処理を登録しておいて、暇になったら実行してくれる。

pluggaloid

mikutterのプラグイン機構を実現するライブラリ。

delayer-deferred

delayerにnext()とかtrap()みたいなdeferred由来のメソッドを追加したもの。

最後に

mikutterは国産のプロダクトと言うことでソースコードのコメントも日本語が多く、大変読みやすいものだと思っています。
8/3のとしぁさんのセッションに洗脳感銘を受けた方。Rubyを勉強してみたい方。

どうぞ臆せずコードの海へ飛び込んでもらえればと思います。

ラズパイ内蔵テンキー「Keybow」のファームウェアをRaspbian化する

この度、Raspberry Pi Zero W搭載のテンキーボード「Keybow」を購入しました。

f:id:moguno:20190103230356j:plain:w300

LUAスクリプトで複雑なキーマクロが組めるのが特徴の製品ですが、コントローラがラズパイなのでもう何でもできるじゃんと思ってポチー!

製品の特長はあっきぃさんのブログが詳しいです。

akkiesoft.hatenablog.jp

しかしながらこれ。
ファームウェアに通常のRaspbianじゃなくて専用の軽量OS「Keybow OS」が採用されているのですが、まぁ出来ることが少ないと。

例えば無線LAN経由でRqspberry Pi Zero Wにsshでログインしてキーコンフィグファイルを直接編集したいなーと思っても、ざっとwpa_supplicant,openssh_server,vim,nanoなんかが足りないと。apt-getもgccも無いから割と詰んでるなぁと。

よく考えたらこれ別にOSを軽量化する必要あんまりなくね?と思ったので、KeybowのOSをRaspbianに差し替えることにしました。

方針

keybowコマンド

Keybowのキー入力やLEDの点灯は、/boot/keybowと言うC言語で書かれたコマンドで行っています。
Keybow OSからkeybowコマンドと動作に必要なLUAスクリプト群を移植すればよさそうです。

USBガジェットドライバ

Keybow OSはRaspberry Pi Zero WのUSB OTG機能を使って自身をHIDデバイス(キーボード)に見せかけています。
これにはlibcomposite.koと言うドライバが使われているので、これをRaspbian起動時にロードするようにします。

余談

ラズパイでUSB OTGシリアルコンソールを使うときはg_serialと言うドライバを使うので、てっきり類似品のg_hidを使ってると思って新年からハマりました。g_の付くドライバは設計が古く、最近はlibcompositeを使うのがナウいみたいです。

やってみる

Raspbian Stretch Liteをインストールする。

現時点で最新の2018/11/13版のRaspbian Stretch Liteを普通にインストールします。

GitHubからKeybow OSのファームウェアをダウンロードする。

$ cd /tmp/
$ wget https://github.com/pimoroni/keybow-firmware/archive/v0.0.2.tar.gz
$ tar xvf v0.0.2.tar.gz

ファームウェアからkeybowコマンド関係のファイルをコピーする。

$ cd keybow-firmware-0.0.2/sdcard
$ chmod 755 keybow
$ sudo cp -p keybow /usr/local/bin/
$ sudo cp -rp keys.lua keybow.lua default.png layouts/ patterns/ snippets/ /boot/

keybowを起動するsystemdのユニットファイルを作る。

/etc/systemd/system/keybow.service
[Unit]
Description=Keybow Daemon

[Service]
ExecStart=/usr/local/bin/keybow
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

起動時にユニットファイルが実行されるようにします。

$ sudo systemctl enable keybow

/boot/cmdline.txtと/boot/config.txtを編集する

Raspbianの起動時にUSB OTG用ドライバdwc2と件のlibcomposite.koをロードするように記述します。

/boot/config.txt(dtoverlay=dwc2を追加)
/boot/cmdline.txt(行の末尾にmodules-load=dwc2,libcompositeを追加)
dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=ffb833f2-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait modules-load=dwc2,libcomposite


ここでRaspbianを再起動して、KeybowのLEDが七色に輝きだしたら成功です。

IFTTTにGoogle Apps Scriptを混ぜたらヤバい化学反応が起こった件

はじめに

2010年12月に彗星のごとく現れたWebサービス同士のマッシュアップサービスIFTTT。
「if (this) then (that)」と言うシンプル極まる構文で手軽にWebサービスが連携出来るのが特徴です。

IFTTTは様々なWebサービスをサポートしており、ちょっと挙げるだけでもTwitterEvernoteGoogle DriveSkypeなどの有名どころから、ナニコレ?ってものまで実に多彩です。

加えて、IFTTT独自のサービス群も魅力的です。
スマホ通知、Alexa・Google Homeの音声入力、スマホウィジェット(メモ、ボタン、写真)など。

それらを前述のシンプルな構文で自由に組み合わせられるのが、IFTTTの魅力だと思います。

IFTTTの弱点

シンプルさが売りのIFTTTなのですが、それがそのまま弱点にもなっているという印象です。

例えば「為替の情報を1時間に1回Pushbulletに通知したい」と言う比較的シンプルな要求はIFTTTでは実現できません。

為替の情報は「if (this) then (that)」のthisにFinanceサービスを指定すれば取得できますが、Financeサービスは「1時間に1回為替情報を報告する」と言うトリガーを持っていないからです。(1日1回トリガーはある。)

こんな感じで、イベントが発生するタイミングはthisのサービスに依存しており、ユーザーが自由に制御できません。

IFTTTではスマホウィジェットや各種スマートスピーカーを使って手動でトリガを掛けることもできますが、それ自身がthisを消費してしまうので、例えば下の様なアプレットは実現できません。

ボタンが押されたら(this) 明日の天気を(×指定不可) SMSに送信する(that)

シンプルさの引き換えに、少しでも凝ったアプレットは実現できない。これでは魅力が半減です。

IFTTTの弱点を克服する

そんな「なんでもは出来ないわ。出来ることだけ。」のIFTTTですが、その中に「Webhooks」と言う興味深いサービスがあります。

「Webhooks」はWebhookを送受信するサービスです。

と、言うことでIFTTTがサポートする以外の何かと連携が可能です。

これを上手く使えば、

為替の情報を1時間に1回Webhookする外部サービス -> Webhookを受信したら(this) Pushbulletに通知する(that)

ボタンが押されたら(this) Webhookを送信する(that) -> Webhookを受けて明日の天気をWebhookする外部サービス -> Webhookを受信したら(this) SMSに通知する(that)

が実現出来るということです。

外部サービスに為替や天気を取得する処理が必要な反面、IFTTTの豊富なthisやthatを組み合わせられるのは非常に魅力的です。

試してみる

と言うわけで、今回は「為替の情報を1時間に1回Webhookする外部サービス -> Webhookを受信したら(this) Pushbulletに通知する(that)」奴を作ってみようと思います。

「Webhookする外部サービス」にはGoogle Apps Script(GAS)を使います。

GASはGoogleさんのサーバでJavascriptを動かせるサービスで、Webhookを送受信するようなサービスが無料で作れちゃいます。
自宅サーバVPSを用意しなくてもインターネット上でちょっとした処理が動かせるので、とても使い勝手が良いです。

IFTTTのアプレット

thisの設定

(1)New Applet画面のthisをクリックする。
f:id:moguno:20180802155231p:plain

(2)Webhooksを選択する。
f:id:moguno:20180802155234p:plain

(3)Receive a web requestを選択する。
f:id:moguno:20180802155239p:plain

(4)イベント名を聞かれるので、今回はsample_triggerとする。
f:id:moguno:20180802155241p:plain

thatの設定

(1)New Applet画面のthatをクリックする。
f:id:moguno:20180802155245p:plain

(2)Pushbulletを選択する。
f:id:moguno:20180802155250p:plain

(3)Push a noteを選択する。
f:id:moguno:20180802155253p:plain

(4)Pushbulletに送信するテキストを設定する。
{{Value1}}がWebhook経由で渡されたデータになります。
画面上は白背景で表示されています。

f:id:moguno:20180802155259p:plain

Webhook用のURLの確認

WebhooksサービスのDocumentationボタンから、Webhook用のURLを確認しておきます。
f:id:moguno:20180802160529p:plain

https://maker.ifttt.com/trigger/sample_trigger/with/key/12345678が今回のURLになります。
f:id:moguno:20180802160520p:plain

GASのスクリプト

IFTTTのWebhookにPOSTする処理

// IFTTTにWebHookをPOSTする
function sendIFTTTWebHook(endpoint, value) {
  var message = {
    "value1":value
  };

  var options = {
    "method":"POST",
    "headers": {
      "Content-Type":"application/json"
    },
    "payload":JSON.stringify(message)
  };
  
  UrlFetchApp.fetch("https://maker.ifttt.com/trigger/" + endpoint + "/with/key/12345678", options)
}

為替情報を得る処理

// JPYUSDを得る
function getUSDJPY() {
  var text = UrlFetchApp.fetch("https://www.gaitameonline.com/rateaj/getrate").getContentText();
  var data = JSON.parse(text);
    
  var quotes = data["quotes"]
  
  var usdjpy = quotes.filter(function(item) {
    return item["currencyPairCode"] == "USDJPY";
  })[0];
  
  return usdjpy;
}

メイン処理

// メイン
function main() {
  var usdjpy = getUSDJPY()
  
  sendIFTTTWebHook("sample_trigger", "ドル円" + usdjpy["bid"]);
}

GASのスクリプトエディタの[編集] - [現在のプロジェクトのトリガー」で、main()が1時間周期で実行されるようにします。
f:id:moguno:20180802160953p:plain

おわりに

これで、為替の情報が1時間に1回Pushbulletに通知できるようになりました。

Pushbulletの扱いはIFTTTに任せているので、GAS側は非常に簡素なコードになっています。

今後「PushbulletじゃなくてTwitterに投稿したい!」となっても、IFTTTにthatをTwitterにしたアプレットを作るだけで対応が可能です。
また「1ドルが112円以上かつ113円以下の時だけ通知させる」みたいな複雑な条件は、GASでサクッと書いてしまえばいいでしょう。

IFTTTの手軽さとGASの自由度が融合した、とても扱いやすい仕組みが出来たと思います。

ところでこの仕組み、UNIXに於けるパイプラインに酷似していると思いませんか?

これこそ、2000年代からみんなが追い求めていた「Web2.0」の一つの到達点ではないかと思いました。