mikutter blog

mikutterのアナウンスなど

mikutter 3.6の新機能

mikutter 3.6の季節がやってきました。いやー、12月はこれのためにかなり予定を開けたんですが、3.6でやりたいことが多すぎて全然間に合いません。

しかし今回はmikutterの一つの到達点にして、転換点になると思っています。世界を手に入れる準備はできましたか?

World

Worldとは

mikutterはTwitterクライアントではないと折に触れて説明してきましたが、多くの標準プラグインTwitterを前提にしていたため、結局Twitterが最も使いやすいく、(TwitterFacebookといったSNSから、電子メールなどを全て含めた)サービスを追加するプラグインは、幾つかの点において妥協する必要がありました。

  • アカウントの切り替えができない: mikutterはマルチTwitterアカウント機能を提供していますが、あくまでこれはTwitterの話で、他のサービスを扱うプラグインは独自にマルチアカウント機能を持つことになります。ユーザにとっても良い体験にはならないでしょう。
  • Twitterがサービス間のプロトコルとなってしまっている: 他のプラグインと連携する時、他のプラグインTwitterにたいして作用するようにできているため、どんなサービスもTwitterに擬態する必要があります。しかしメールにはふぁぼはありませんし、逆にTwitterにはBCCはありません。

結局、プラグインが他のプラグインTwitter相当の機能を仮定していたmikutterで得られる、他のサービスに対応するプラグインの体験は、良いものではありませんでした。今までそういったプラグインを使ってきた人なら実感しているところでしょう。

そこで、mikutterは、全てのサービスに仮定してもいいような機能しか持たない最低限の仮想的なサービスを定義し、全てのプラグインは一旦そのサービスを前提に実装したうえで、例えばTwitterリツイートのような、サービス独自の機能を拡張していくというスタンスを取ることにしました。この仮想的なサービスをもとに実装したサービスが「World」と呼ばれます。

World系プラグイン

Worldはプラグインによって提供されます。これはつまり

Twitterプラグイン

もちろん、Twitter機能も全てtwitterプラグインに移植され、Twitter Worldを提供するように書き換えられました。 今までは、Twitterを使っていないmikutterユーザも、Twitter関連機能が常に表示されているという問題がありました。今後mikutterでTwitterを使わない場合は、twitterプラグインを外して利用できます。

また、このことによって3.6で出来なくなることはありません。Worldにはそれだけの力があるということです。

Serviceの削除

mikutterでTwitterアカウントを管理するためのServiceクラスは削除されました。これは、mikutterからアカウント管理機能が無くなり、worldプラグインで提供されるという意味です。

現在選択しているTwitterアカウントを返す Service.primary などは、互換性のために提供されているので、Twitterクライアントとして使うぶんには、プラグインを修正する必要はありません。

Diva

Retrieverを廃止。Retrieverをdiva gemにし、それを利用するようにしました。Retrieverという名前でも依然参照できるため、後方互換性はありますが、3.6に対応するプラグインはDivaを使うことを推奨します。

Retrieverになかった、次のような機能があります。

再帰的なModelインスタンスの構築

複雑にオブジェクトが入れ子になったJSONレスポンスを Diva::Model インスタンスに加工するのはRetrieverと違って簡単です。 どちらかというと出来ていなかったほうがおかしかったような機能なのでさらっと。

class Message < Diva::Model
  field.string :body
  field.has User, :author
end

class User < Diva::Model
  field.string :name
end

のようなModelがあったとして、

{
  "body": "本文です",
  "author": {
    name: "toshi_a"
  }
}

のようなJSON文字列をパースした結果が seed に束縛されているとしたら、

m = Message.new(seed)
m.body # => "本文です"
m.user.name # => "toshi_a"

のようなことができます。

Worldを提供するプラグインは、JSON形式でAPIレスポンスを受け取ったら、単にそれをパースしてModelのコンストラクタに渡せばいい感じになるということです。

JSONエクスポート

Diva::ModelはJSON形式でダンプできます。ダンプされるのはフィールドとして定義されているキーだけです。要するに、ModelをJSONにダンプしたものをパースして、同じModelクラスのコンストラクタに与えると、元のModelが復元されるということです。

Delayer Deferred

mikutter 3.6ではDelayer Deferred 2.0を利用します。

全体の大幅な書き直し

もともとこのライブラリはjsdeferredをそのまま直訳したようなもので、classをほとんど使っていないなど、Rubyらしくないコードでした。 そのため、ある複雑なケースでは内部状態の不整合が起こり、幾つかの不具合の原因になっていました。

Deferred 2.0は、APIをそのままに、Rubyの機能を念頭に再設計しました。結果として不具合も取り除かれ、デバッグに十分なエラー報告が行われるようになりました。

async/await

次のようなコードは目的が明確で、読みやすいです。

rss = get_articles("http://....rss")
rss.each do |article|
  tweet(article)
end

素直ですが、get_articlesが通信をしている間、UIがフリーズするという問題があります。 mikutterではget_articleのようなメソッドがDeferredを返すように実装することで、この問題を解決しています。

get_articles("http://....rss").next{ |rss|
  rss.each do |article|
    tweet(article)
  end
}

Deferred 2.0では、Deferredのコンテキストで別のDeferredに対してawaitメソッドを使うことで、その戻り値を取り出せます。

Deferred.new do
  rss = get_articles("http://....rss").await
  rss.each do |article|
    tweet(article)
  end
end

awaitメソッドは、単項+演算子で代用できます。

Deferred.new do
  rss = +get_articles("http://....rss")
  rss.each do |article|
    tweet(article)
  end
end

今までDeferredを受け取っていた全ての箇所で使えますし、次に紹介するSpellの戻り値も全てDeferredなので、これを利用できます。

Spell

「メンションする」「お気に入りに追加する」「リツイートする」「検索する」といった、1つ以上のModelの集合に対して行う手続きのことを Spell と名付けました。Spellは、今までModelに定義されていましたが、3.6からは、プラグインで定義します。

Modelに手続きをもたせることの弊害

それが可能か・実装されているかを調べる安全な方法がない

3.5以前はリツイートしたい場合、 message.retweet(service) のようなコードを書いていましたが、リツイートできるか否かを事前に判断するには message.retweetable?(service) のようなコードでチェックしていました。retweetとretweetable?で規則的ではありますが名前が変わってしまいます。例外なく「〜able?」になっている保証もなければ、提供しないことも可能でした。

レシーバとしてどちらが適切?

また、 message.retweet(service)service.retweet(message) のような、主従が逆になっているものはどうでしょう。直感的にはこれはどちらも正当で、同じ意味です。しかし実際には、両者に全く違う実装をすることはできます。そもそも、リツイートをするといった時、リツイートをするTwitterアカウントと対象に明確な主従関係はありません。かといって、サードパーティプラグインがModelを定義できるmikutterにおいて、入れ替えても等しい動作をすることを期待するのは現実的ではありません。

拡張できない

mikutterはあらゆるものがプラグインを使って実装されており、全てのプラグインのやりとりをフックすることができるため、これだけの拡張性を持っています。

しかし、Modelの檻に囚われたSpellたちは、そのModel自身で定義される必要があります。つまりModelを定義したプラグインが提供しない機能があった場合に(具体的にはTwitterのブロック機能など)、ブロック機能をサードパーティプラグインが追加する方法がないのです。

ではTwitterプラグインがブロック機能を実装すべきかというと、それはそうかもしれませんが、他のプラグインがそれを拡張できないという信じがたい現実から目を背けていても根本的な解決にはなりません。Modelに定義したメソッドは拡張できないため、Modelはせいぜいデータに簡単な加工を加えて返すメソッドくらいしか定義されていない、ただの容れ物として扱うのが理想です。

Spellをプラグインで定義する

mikutter 3.6では、これらをSpellと名付け、Modelではなくプラグインで定義するようにしました。

Plugin.create(:sample) do
  defspell(:retweet, :twitter, :twitter_message,
           condition: ->(twitter, message){ !message.protected? }
          ) do |twitter, message|
    (twitter/:retweet/:add).message(id: message.id)
  end
end

condition: 引数で、ブロックが呼ばれる前提条件を設定することができます。conditionがfalseを返せば、ブロックが呼ばれずに呼び出しは失敗します。

Spellを呼び出す

プラグインのコンテキストで、呼び出したいSpellの名前に一致するDSLメソッドが定義されているため、たんに retweet と書きます。 具体的には、以下のように呼び出します。

Plugin.create(:sample) do
  retweet(twitter, message)
end

Spellの名前と、引数の種類さえ合っていれば、引数の順番は任意です。この性質によって、どちらがレシーバであっても同じ動きをするように、二箇所に実装を行う必要がなくなりましたし、呼び出す側も順序を気にする必要はありません。 これらは必ずDeferredを返すため、以下のように成否を受け取ることもできます。

Plugin.create(:sample) do
  retweet(twitter, message).next{|retweet_message|
    # リツイートできた場合
  }.trap{|err|
    # エラーの場合
  }
end

Spellの名前に「?」をつけると、defspellのcondition引数のブロックの評価結果を返します。実際にはretweetせず、retweetの前提条件を満たしていることを確認する時に使います。

Plugin.create(:sample) do
  retweet?(twitter, message) # => true or false
end

Spellの名前と、ローカル変数などの名前が重複していると、Spellよりローカル変数が優先されます。 その場合は、 spell. を先頭に付けることで呼び出せます。

Plugin.create(:sample) do
  spell.retweet(twitter, message)
  spell.retweet?(twitter, message) # => true or false
end

Form DSL

Setting DSLの機能の大半が、Form DSLに抽出されました。 Setting DSLの機能としてはできることが増えただけですが、Form DSLを使ったDialog DSLが登場したため、ユーザにモーダルダイアログを表示して値の入力を要求できるようになりました。

Dialog DSL

プラグインDSLの中で、モーダルダイアログを表示するユニバーサルな方法が提供されました。いままでダイアログボックスを利用するプラグインgtkプラグインに依存していましたが、Dialog DSLを使えばguiプラグインへの依存のみとなります。

dialog("ダイアログサンプル") {
  select "新幹線の種別", :train do
    option "ノォゾォミィ", "のぞみ"
    option "ヒィカァリィ", "ひかり"
    option "コダァマァ", "こだま"
  end
  select "行き先", :station do
    option "トーキョー", "東京"
    option "ナァゴォヤ", "名古屋"
    option "シィンオォサァカ", "新大阪"
  end
}.next{ |obj|
  # positive buttonが押された場合
  p obj #=> {success: true, train: "ノォゾォミィ", station: "トーキョー"}
}.trap{ |exception|
  # キャンセルされた場合
  p exception #=> {success: false}
}

上記例のように、かなり複雑な入力をさせるダイアログボックスを、設定と同じようにGtkなどに依存しない形で書くことができ、結果をDeferredで受け取れます。

ダイアログボックスはユーザが行っている操作を中断させてしまうため、極力利用は避けるべきです。 mikutterにおけるありがちな誤用として、「○○が完了しました」といった通知に使ってしまうというのがあります。これは、ユーザに入力や選択を迫らないし、ユーザは無視してもいいので、不適切です。この用途にはactivityを使うべきです。

新たなウィジェット

Form DSLはSetting DSLと同等ですが、3.6ではいくつかウィジェットが追加されており、設定やダイアログボックスで利用できます。

photoselect

f:id:toshi_a:20171207002701p:plain

Photo ModelのURIとして適切な文字列を入力させるウィジェットです。抽出タブのアイコンの設定に使われています。

fileselect同様、ローカルファイルピッカーもついています。このピッカーは画像ファイルしか選択できなくなっているので、ユーザは画像を探しやすくなります。

f:id:toshi_a:20171207002757p:plain

さらに、現在入力されているパスにある画像をサムネイル表示する機能もあります。このサムネイルをクリックするとIntentが発行されるので、画像ビューアプラグインなどで拡大表示できます。

ラベル

ユーザに値を入力させるものではありませんが、単なるテキストを配置するためのものです。設定以外に用途が広がったことで追加されました。

modelビューア

f:id:toshi_a:20171207002933p:plain
おなじみの投稿削除確認ダイアログボックス。dialog DSLを使って実装し直され、削除しようとしているツイートがわかりやすくなった

これも入力ではありませんが、Modelを表示する機能です。アイコンとdescriptionテキストが配置されます。

クリックすると、そのModelに対してIntentが発行されます。WebヘルプなどのURLへのリンクや、ツイート削除の確認ダイアログで使われています。

Photo Modelの機能拡張

意味的には同じ画像が、違うサイズや形式で複数枚提供されている場合、それらを単一のPhoto Modelで扱うことができるようになりました。環境によってはネットワークトラフィックやメモリが削減できたり、mikutterで閲覧する画像が以前より高画質になります。

具体的には、Twitterの添付画像にはいくつかのサイズバリエーション(variant)が用意されていて、mikutter 3.5では最大(orig)より一段階小さい標準サイズを常に使用していました。 Photo ModelからPixbufなどの画像を直接得る時、要求されているサイズより大きい最小のvariantの画像をPhoto Modelが自動的に判断して、ダウンロードするようになりました。photoselectに表示する時は最小のサムネイル、タイムラインに画像をインラインプレビューするプラグインでは中サイズ、画像ビューアで開いたら最大サイズという具合に、適切なvariantが自動的に使われます。

ユーザとしては、十分なサイズの画像が常に使われるようになるし、開発者としては何も考えなくてもこういった調整が行われるようになります。

タイムラインに表示するMessageの数の調整

現在は200件を越えると表示領域から削除されていましたが、1件から10000件の間で設定できるようになりました。

抽出タブの並び順を変更する設定

f:id:toshi_a:20171207010418p:plain

抽出タブの設定で、タブ毎にTLの並び順を変更できるようになりました。デフォルトは今までと変わらず、更新日時順(ふぁぼやリツイートをされると一番上に来る)です。

この他に、投稿日時順(ふぁぼやリツイートの影響を受けない)が追加されています。

設定メニューの階層化

設定画面は、ものによっては一つの設定メニューの中に大量の設定項目があり、目的のものを見つけづらい場合がままありました。

たとえばアクティビティは、プラグインが拡張したアクティビティロールの数だけ設定が増えていくので、多数のプラグインを利用している人はこの項目が多くなりがちです。

f:id:toshi_a:20171220214551p:plain

mikutter 3.6では、 settings ウィジェットが設定の中で使われていると、サイドバーの設定メニューにツリー表示してくれます。

f:id:toshi_a:20171220212804p:plainf:id:toshi_a:20171220214602p:plain

上位メニュー(例ではアクティビティ)を選択していると、今までと同じ表示ですが、ツリーを開くと、右の設定項目の中で、囲まれている部分毎にメニューが作られていることがわかります。

f:id:toshi_a:20171220213356p:plainf:id:toshi_a:20171220214614p:plain

下位メニュー(例ではリツイート)を選択すると、その囲まれた中だけが表示されます。

設定DSL自体には変更はありませんし、過去に書かれたプラグインも、3.6では自動的にこのような表示になります。

しかし、設定のためにGtkを直接使っているプラグインは、この機能の対象外です。もしGtkを直接使わなくても差し支えがないなら、使わないような実装に変更することをお勧めします。

PostBoxやWorld Shifterの表示設定

PostBox(画面上の投稿ボックス)やWorld Shifter(画面左上のアカウント切り替え)の表示・非表示を設定できるようになります。今までは2つ以上WorldがないとWorld Shifterが表示されていませんでしたが、設定すれば表示出来るようになります。

f:id:toshi_a:20171220214301p:plain

Worldが一つしかない場合に表示していても一見意味がなさそうですが、3.6ではWorld ShifterからもWorldの追加画面が呼び出せるようになりました。

Twitter Plugin

mikutterのコア機能として埋め込まれていたTwitter関係の機能が、全てTwitterプラグインに移植されました。mikutterのTwitter依存をほとんど取り去ることで、本体をシンプルに保つことが出来、見通しが良くなると考えています。

Ruby 2.3以降をサポート

Ruby 2.2は2018年3月末にサポートが終了することが発表されています。 少し早いですが、mikutter 3.6はRuby 2.3を最低動作要件とします。

リリース時期

mikutter 8周年となる12/25を予定していますが、ここまでには十分な品質のものは完成しないでしょう。デバッグが終わっていない状態、既知のバグがある状態でのリリースとなることが予想されます。

ここ数年のmikutterは、12月は数度のα版リリースを繰り返し、試用した一部ユーザにバグ報告を貰って、見つけられる範囲のバグを取り除いていました。それでも正式にリリースされて初めて使う人が大半なので、ほとんどのバグはリリース以降に見つかっていたのですが。

今年は、12/25にリリースされる 3.6.0 がα1みたいなものです。この記事は簡潔にまとめましたが、相当なボリュームのアップデートとなるので、いつにも増してドッグフーディングしましょう。

今後の予定

11月18日朝、私が9年間使っていたTwitterアカウントが凍結されました。私自身は、あるいは尊敬する、あるいは優秀な、あるいは面白い人が消えていき、大好きな自分のタイムラインが確実に衰えていっているのを唇を噛み締めながら見ていました。それに比べれば自分のアカウントが凍結されたことなど取るに足らない出来事でしたが、いくらかの人のタイムラインに小さな影響を与えたということを一部のネット逆イタコからきいています。

mikutterユーザは、私がモチベーションを失ってmikutterの開発に影響を与えるのではと心配してる人もいるのではないでしょうか。個人的なことを書くなら別のブログでやるべきと思うので、何故それが杞憂なのかを手短に書きます。

mikutterの存続

今まではmikutterでTwitterをしながら、Twitterクライアントとしてのmikutterを育ててきました。mikutterはTwitterによってここまで大きくしてもらえたのです。生みの親より育ての親ともいいますが、mikutterはその拡張性を生かしてTwitterクライアントのようになることができ、今では「Twitterクライアントではない」と弁明しなければならないほどになりました。

mikutter 3.6というのは、上で述べたWorldプラグインが一応の完成に漕ぎ着けた記念すべきバージョンです。これからは、Twitter以外のWorldを積極的に利用しながら、mikutterを育てていくことになるでしょう。

凍結がもし1年早ければ、流行に疎い私はまだMastodonを知らなかったでしょうし、mikutter 3.5では十分な準備はできておらず、TwitterクライアントをMastodonクライアントに書き換えるような誤った選択をし、mikutterは息絶えていたかもしれません。今回のことは、もはや独り立ちできるのにTwitterのすねをかじって生きていくつもりだった私への叱咤のように思えてなりません。

名前もmikutterにしてしまい、Twitterに傾倒しすぎたかと反省していた時期もありましたが、8年間もTwitterに育ててもらったソフトウェアが他にどれだけあるでしょうか。Mastodonクライアントのようになったとしても、「tterってwwwBANされたのにwww」と馬鹿にされることがあったとしても、関係ありません。mikutterしか、mikutterを名乗る資格のあるクライアントはありません。黎明期を支え散っていった英雄たちでも、パッと出の公式クライアントでも、買収されたクライアント でもなく、mikutterこそが、最もmikutterなのです。

Twitterプラグインのサポートについて

私事ばかりで恐縮ですが、私は9年で30万以上ツイートしていました。これは単純に割れば毎日100以上ツイートしていた計算です。魚から水を奪っても歩き出すようなことはありません。私には新たな水が必要です。そこで、mikutterユーザが集まってTwitterのように雑談するMastodonインスタンスを用意しました

このインスタンスは「mikutterのMastodonインスタンスが欲しい」という希望を @ahiru@social.mikutter.hachune.net が叶えてくれたもので、彼を中心にmikutterユーザが運営しています。

その上で、Twitterプラグイン(=Twitterのサポート)がどうなっていくかは、FAQを参照してください。

https://mikutter.hachune.net/faq

試して

develop というブランチをチェックアウトすれば試用できますが、World機能については、初回起動時のチュートリアルに問題が出るため topic/981-world ブランチ、Spellについてはまだ複数の課題が残っているためpushされていません。12/20に、全てのリリース予定の機能をdevelopブランチにmergeしているので、試用して不具合に気づいた人は、バグ報告をお願いします(後述)。12月になっても作ってる途中とは…とも思わなくもないんですが、ブログを見ていると、12月にデバッグしかしていない状況というのはここ2〜3年だけだったようです。

いつも、バグ報告はTwitterでは見落とす可能性があるためRedmineでお願いしていますが、前述の通りもう私はTwitterを見ることはないです。前述のMastodonインスタンスの私のアカウントでも作業状況などを随時Tootしていますが、Twitter同様リプライに気づかないことが多いため、Redmineでお願いします。

それでは、mikutter3.6でお会いしましょう。