マストドンを改造する→楽しい


この記事は Mastodon Advent Calendar 2018 の 11日目の記事です。
(この記事に含まれているソースコードはマストドンによってライセンスされています。)

ちょっと忙しくしててあまり記事書けてませんでした。去年のアドベントカレンダーは無理しすぎて、来年は絶対に書かないぞ!と意気込んだものの結局このように書いています。(抑えたけど)

今年はマストドンを改造したので、その時の解説をしようと思います。
あ、ちなみに改造といってもマストドンはOSSなのでライセンス守れば自由に改造ができるので安心してください。

作ったもの

G+風のタイムラインです。かなり富豪的な実装をしたので実装自体はよくありません汗

コードはこちらにあります
現在、v2.5.2に対応しています。

改造をする前に

改造を始める前に、リポジトリのブランチの管理が大事になります。
よくなってしまいがちなのが、マストドン本体のコミットに積み重ねていくように自分のコミットを重ねていくことです。下記のようだと、メンテナンスが大変になります。

そして、どこが自分の変更なのか、マストドン本体のコミットなのかさえわからなくなって収集がつかなくなります。やってくるのはコンフリクトの嵐でしょう。

必ず、マストドンの変更が含まれたブランチと自分の変更が含まれたブランチは分けておき、自分のブランチのコミットはマストドンのブランチより必ず一番上になるようにしましょう。

この状態を維持するには、マストドンのアップデートの度にすべての変更を捨ててタグをチェックアウトしてまっさらなブランチを用意し、そこに別で用意していた自分のコミットを含んだブランチをmerge ではなく rebase します。

rebaseすることで、そのブランチに含まれた異なる歴史の部分である自分のコミットが一番上へ移動します。移動と言いましたが、リビジョンIDも書き換わるので「今」に歴史を再現して過去の歴史はなかったことにするようなイメージです。

これで、マストドンのコミットと自分のコミットが混じり合うようなことを避ける事ができ、検証や開発がしやすくなります。あくまで、改造部分は、プラグインや拡張といったかんじでいつでも外せるようなものであるように管理するのが良いです。

改造する前にその2

改造にとってこわいのがやはりコンフリクト。いくらリポジトリ管理をちゃんとしてるからとはいえ、コンフリクトは起きてしまうものです。このコンフリクトをできるだけ避ける方法があります。同じ機能をもう一つ作ってしまうことです。

コピペです。根本的な部分からファイルを分けることによって、その機能を完全に独立させたものとしてしまうことです。

マストドンにおける根本的な部分は、あくまで主観ですがapp/javascript/mastodon/features/ui/index.js にあります。

ColumnsAreaContainer というのがあり、ルーティングを構成しています。ルーティングというのはページのURLに応じて表示させるコンテンツを切り替えるような仕組みを提供するものです。

私はここに

<WrappedRoute path='/timelines/public/gplus' exact component={GplusCommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />

というルーティングを作りました。
実は、大本からガッツリ分けてました。そして、どのようなコンテンツを表示させるのかというところですが、 component= の部分がまさにそれで、 GplusCommunityTimeline を指定しています。

表示しているコンテンツさえ、オリジナルに作り変えています。
先程からコンテンツと言っていますが、これはコンポーネントのことであり、Mastodonを動かしているReactjsの要素です。

改造する前にその3

ソースコードのほうで app/javascript/mastodon/features/ui/containers/gplus_status_list_container.js というファイルを作っています。そこに makeGetStatusIds という関数がありますが、これで、タイムラインに流すトゥートを決めているようです。

status というのがトゥートのことで、 createSelector は 大きなトゥートの塊からそのタイムラインに流すものを決めてるような形です。

例えば、

    if (columnSettings.getIn(['shows', 'reblog']) === false) {
      showStatus = showStatus && statusForId.get('reblog') === null;
    }

こちらは、リブログ、つまりブーストの場合は流さないようなことが書かれています。columnSettingsは、ブーストを非表示にするかどうかの設定を読み取っているようです。

    if (columnSettings.getIn(['shows', 'reply']) === false) {
      showStatus = showStatus && (statusForId.get('in_reply_to_id') === null || statusForId.get('in_reply_to_account_id') === me);
    }

こちらは、リプライを非表示、だけど自分へのリプライは表示といった内容です。
タイムラインで流すものを決めていきたい場合はこのあたりを改造していくと良いでしょう。

改造する前にその4

app/javascript/mastodon/selectors/index.js というファイルがありますが、
ここに inReplyParentSearch という関数を追加しました。

Immutable.jsを利用しており、トゥートのin_reply_to_id を辿っていき再帰処理をするという内容になってます。

こちらは、 gplusMakeGetStatus で呼ばれ、 更に app/javascript/mastodon/containers/gplus_status_container.js で使われています。

改造する前にその5

Statusまわりの改造は、無理にいろいろとやりすぎるとフロントがとても重くなってしまうので、その機能が必要でない人にもパフォーマンスの面で影響がでないように配慮することが大切です。

他にも、ReactJsだけでなく Rails側の改造もありますが、今回はフロント部分のみを改造してみる方法を紹介してみました。