Twitterクライアント"Grimoire"をプラグインで拡張する

![Screenshot from 2013-12-05 02:31:32.png](https://qiita-image-store.s3.amazonaws.com/0/31405/a7dec581-9bd9-79a8-88a4-e6fda9c52ec8.png)

## ご挨拶

 初めまして，qiitaでの投稿は初になりますBOXPです．
 今回は，Clojureで作られたTwitterクライアント"Grimoire"でプラグインを自作する方法について書きます．

## Grimoireについて

Grimoireは，Clojure+Twitter4J+JavaFxで作られたTwitterクライアントです．
タイムラインの閲覧等のTwitterの基本となる操作は勿論，以下の方法による柔軟な拡張が可能です．

 - 設定ファイルによる拡張(Vim,Emacsライクな設定ファイルによる拡張)
 - Evalモードでのコマンド入力による拡張
 - プラグインによる拡張

この内設定ファイルによる拡張とEvalモードでの拡張については既に[Hatenaブログの記事](http://boxp.hatenablog.com/entry/2013/10/16/192000)で解説してあるので，今回はプラグインの拡張について書きます．

## プラグインについて

Grimoireでは，プラグインに対して以下の機能を提供します．

 - ツイート取得，ふぁぼられ等のTwitterイベントへの処理
 - 右クリックメニューの追加
 - Grimoire起動時の処理

Twitterイベントへの処理を追加する事で，受信されたツイートに対して自動で返信を行ったり，見たくないツイートを隠したり，Clojureが許す限りの拡張が可能です．
 現時点でGrowlへの通知を行うプラグインや，ツイートをクリップボードにコピーするプラグイン等が既に公開されています．

## プラグインの作り方

プラグインは，Clojureのprotocolである"Plugin"プロトコルを継承する事で作成出来ます．
Pluginプロトコルは以下のメソッドを持ちます．

 - (get-name [this])
   - 右クリックメニューのボタンとなる文字列を返すよう実装する事で，右クリックメニューを追加します(nilを返すよう実装すると，ボタンを追加せずにプラグインを読み込みます)
 - (on-status [this status])
   - 新着ツイートを受信した際に呼ばれます．引数としてツイートがtwitter4j.Statusのインスタンスで与えられます
 - (on-reply [this status])
   - 新着ツイートを受信した際に呼ばれます．引数としてツイートがtwitter4j.Statusのインスタンスで与えられます
 - (on-rt [this status])
   - 公式リツイートをされた際に呼ばれます．引数としてリツイートがtwitter4j.Statusのインスタンスで与えられます．
 - (on-unrt [this status])
   - 公式リツイートが取り消された際に呼ばれます．引数として取り消されたリツイートがtwitter4j.Statusのインスタンスで与えられます．
 - (on-fav [this source target status])
   - 自分のツイートがお気に入りに登録された際に呼ばれます．引数としてお気に入りをしたアカウントと受けた側のアカウントのtwitter4j.Userのインスタンスと，ツイートがtwitter4j.Statusのインスタンスで与えられます．
 - (on-unfav [this source target status])
   - 自分のツイートのお気に入りが解除された際に呼ばれます．引数としてお気に入りを解除したアカウントと受けた側のアカウントのtwitter4j.Userのインスタンスと，ツイートがtwitter4j.Statusのインスタンスで与えられます．
 - (on-del [this status])
   - ツイートが削除された際に呼ばれます．引数として削除されたツイートがtwitter4j.Statusのインスタンスで与えられます．
 - (on-follow [this source user])
   - 他のアカウントからフォローされた際に呼ばれます．引数としてフォローをしたアカウントとフォローされたアカウントがtwitter4j.Statusインスタンスで与えられます．
 - (on-dm [this dm])
   - ダイレクトメッセージを受信した際に呼ばれます．引数として受信したダイレクトメッセージがtwitter4j.api.DirectMessagesResourcesインスタンスで渡されます．
 - (on-start [this])
   - Grimoireが起動する時に呼ばれます．
 - (on-click [this e])
   - 右クリックメニューからボタンがクリックされた際に呼ばれます．get-nameがnilを返す時，このメソッドはスルーされます．引数として，イベントオブジェクトがjavafx.event.EventHandler<ActionEvent>インスタンスとして呼ばれます．

試しに，特定のツイートに対して自動でふぁぼるプラグインを作ると，以下のような形になります．

```clj
 (reify Plugin
   (get-name [this] "自動ふぁぼ")
   (on-status [this status]
     (if (= (. status getText) "ておくれ")
       (fav (@tweets status))))
   (on-rt [this status])
   (on-unrt [this status])
   (on-fav [this source target status])
   (on-unfav [this source target status])
   (on-del [this status])
   (on-follow [this source user])
   (on-dm [this dm])
   (on-start [this])
   (on-click [this e]))
```

プラグインは，一つのファイルに一つのプラグインオブジェクトを配置する事で動作します．
ファイル全体で一つのプラグインオブジェクトを返す形にすればOKなので，use,import等によるライブラリの使用する場合，若しくは関数を宣言しておきたい場合等は，以下のGrowl通知プラグインのように，doマクロで包むことも可能です．

 ```clj
  (do 
  (require '[gntp :as gp]
           '[clojure.java.io :as io])
  (let [growler (gp/make-growler "Grimoire")
        notifier (if (try (.. @main-stage isShowing) 
                       (catch Exception e nil))
                   (fn [& _] nil)
                   (growler :default {:icon (io/input-stream (io/resource "alice.png"))}))]
    (reify Plugin
      (get-name [this] nil)
      (on-status [this status] 
        (cond
          (.. status isRetweet)
          ((:default notifier) (str "@" (.. status getRetweetedStatus getUser getScreenName))
            :text (.. status getRetweetedStatus getText)
            :icon (io/input-stream (.. status getRetweetedStatus getUser getBiggerProfileImageURL)))
          :else 
          ((:default notifier) (str "@" (.. status getUser getScreenName))
            :text (.. status getText)
            :icon (io/input-stream (.. status getUser getBiggerProfileImageURL)))))
      (on-rt [this status] 
         ((:default notifier) (str "@" (.. status gerUser getScreenName) " にリツイートされました！")
        :text (.. status getRetweetedStatus getText)
        :icon (io/input-stream (.. status getRetweetedStatus getUser getBiggerProfileImageURL))))
      (on-unrt [this status] 
         ((:default notifier) (str "@" (.. status getUser getScreenName) " にリツイートを取り消されました！")
        :text (.. status getRetweetedStatus getText)
        :icon (io/input-stream (.. status getRetweetedStatus getUser getBiggerProfileImageURL))))
      (on-fav [this source target status] 
        ((:default notifier) (str "@" (.. source getScreenName) " にふぁぼられました！")
        :text (.. status getText)
        :icon (io/input-stream (.. status getUser getBiggerProfileImageURL))))
      (on-unfav [this source target status] 
        ((:default notifier) (str "@" (.. source getScreenName) " にあんふぁぼされました！")
        :text (.. status getText)
        :icon (io/input-stream (.. status getUser getBiggerProfileImageURL))))
      (on-del [this status] nil)
      (on-follow [this source user] 
        ((:default notifier) (str "@" (.. source getScreenName) " にフォローされました！")
        :text (.. source getDescription)
        :icon (io/input-stream (.. source getBiggerProfileImageURL))))
      (on-dm [this dm] 
        ((:default notifier) (str "@" (.. dm getSenderScreenName) " からDMを受信しました！")
        :text (.. dm getText)
        :icon (io/input-stream (.. dm getSender getBiggerProfileImageURL))))
      (on-start [this] nil)
      (on-click [this e]))))
 ```

プラグインファイルは，~/.grimoire/plugin/に拡張子.cljで保存する事で，起動時に自動でロードされます．（load-pluginを実行する事で，再ロード出来ます）
 
## プラグイン作成に便利なドキュメント

プラグインを作る時は，JavaFx,Twitter4j,Grimoire,Clojureのドキュメントが手元にあると便利です．
以下の４つを開いておけば，大体の物は作れると思います．

 - [Twitter4j javadoc](http://twitter4j.org/javadoc/)
 - [JavaFx javadoc](http://docs.oracle.com/javafx/2/api/)
 - [Grimoire API Documentation](http://archbox.dip.jp/works/grimoire/doc/)
 - [ClojureCheetSheet](http://clojure.org/cheatsheet)

少し急ピッチでしたが，以上でGrimoireでのプラグインの解説は大体おしまいです．
もし需要があればもう少し詳しい解説を載せます…