読者です 読者をやめる 読者になる 読者になる

pockestrap

Web Programmer's memo

mikutter-sub-parts-clientを魔改造した

Programming Ruby Twitter mikutter

toshia/mikutter-sub-parts-client · GitHub
多分みんな入れてるプラグインじゃないでしょうか。
viaを表示するプラグインです。

ただ、このプラグイン、クライアント名をクリックしても何も起きないのですよね。
ブラウザでクライアントのページを開いてほしいなー、とか思うじゃないですか。
実装しました。
pocke/mikutter-sub-parts-client · GitHub

URLが捨てられてる

捨てられてるんですよ、URL。
試しにmikutterコンソールを開いて

Plugin.create :hoge do
  on_appear do |msgs|
    msgs.each do |msg|
      p msg
    end
  end
end

とかしてみると、下記の形式でterminalに受信したツイートが出力されます(見やすいように適宜改行を入れています)。

{
  :created_at=>"Sat Jan 18 09:38:04 +0000 2014",
  :id=>424475770371592192,
  :id_str=>"424475770371592192",
  :message=>"にゃーん",
  :source=>"mikutter",
  :truncated=>false,
  :replyto=>nil,
  :in_reply_to_status_id_str=>nil,
  :receiver=>nil,
  :in_reply_to_user_id_str=>nil,
  :in_reply_to_screen_name=>nil,
  :user=>User(@p_ck_),
  :geo=>nil,
  :coordinates=>nil,
  :place=>nil,
  :contributors=>nil,
  :retweet_count=>0,
  :favorite_count=>0,
  :entities=>{
    :hashtags=>[],
    :symbols=>[],
    :urls=>[],
    :user_mentions=>[]
  },
  :favorited=>false,
  :retweeted=>false,
  :lang=>"ja",
  :created=>2014-01-18 18:38:04 +0900,
  :exact=>false,
  :modified=>2014-01-18 18:38:04 +0900
}

すると、sourceとして"mikutter"がありますが、mikutterが指し示す筈のURLはどこにも見当たりません。
ですが、下記のコードをmikutterコンソールから叩くと、twitterからAPIを叩いた時点ではURLの情報が存在していることがわかります。

(Service.primary.twitter/'statuses/show/424475770371592192').json({}).next do |x|
  p x
end
{ :source=>"<a href="http://mikutter.hachune.net/" rel="nofollow">mikutter</a>", ...}

出力されるHashの中に:sourceというキーがあり、ここにクライアントの情報が入っています。
つまり、mikutterのどこかで:sourceの内容からクライアント名だけが抽出されていると言うことになります。
で、抽出されているのがここです。

##### file: core/lib/mikutwitter/api_call_support.rb
def message(msg)
  cnv = msg.convert_key(:text => :message,
                        :in_reply_to_user_id => :receiver,
                        :in_reply_to_status_id => :replyto)
  cnv[:source] = $1 if cnv[:source].is_a?(String) and cnv[:source].match(/^<a\s+.*>(.*?)<\/a>$/)
  cnv[:created] = (Time.parse(msg[:created_at]) rescue Time.now)
  cnv[:user] = Message::MessageUser.new(user(msg[:user]), msg[:user])
  cnv[:retweet] = message(msg[:retweeted_status]) if msg[:retweeted_status]
  cnv[:exact] = [:created_at, :source, :user, :retweeted_status].all?{|k|msg.has_key?(k)}
  message = cnv[:exact] ? Message.rewind(cnv) : Message.new_ifnecessary(cnv)
  message end

このcnv[:source] = ...行でクライアント名部分だけを抜き出しているようです。
ということで、url部分も取得するようにパッチをあてました。

mikutter の Message に source の URL を含めるパッチ

これで cnv[:source_url] にクライアントのURLが格納されます。
先ほどの用に流れてきたMessageを眺めてみると、

{
  :created_at=>"Sat Jan 18 09:38:04 +0000 2014",
  :id=>424475770371592192,
  :id_str=>"424475770371592192",
  :message=>"にゃーん",
  :source=>"mikutter",
  :truncated=>false,
  :replyto=>nil,
  :in_reply_to_status_id_str=>nil,
  :receiver=>nil,
  :in_reply_to_user_id_str=>nil,
  :in_reply_to_screen_name=>nil,
  :user=>User(@p_ck_),
  :geo=>nil,
  :coordinates=>nil,
  :place=>nil,
  :contributors=>nil,
  :retweet_count=>0,
  :favorite_count=>0,
  :entities=>{
    :hashtags=>[],
    :symbols=>[],
    :urls=>[],
    :user_mentions=>[]
  },
  :favorited=>false,
  :retweeted=>false,
  :lang=>"ja",
  :source_url=>"http://mikutter.hachune.net/",
  :created=>2014-01-18 18:38:04 +0900,
  :exact=>false,
  :modified=>2014-01-18 18:38:04 +0900
}

となっており、:source_urlが確認できますね。
これでプラグインを作成する下準備が整いました。

プラグインを書き換える

最初sub-parts-clientのソースを見た時、なんだこれはわけがわからんーーーって感じでした。
とりあえず、Gdk::SubPartsの宣言でも探してみます。そして、Gdk::SubPartsを使っているコードも探してみます。
……
core/mui/cairo_sub_parts_helper.rb で宣言されていますね。
また、core/mui/の中にそれっぽいファイルがいくつかあるので、まとめて開いてみましょう。

$ vim core/mui/cairo_sub_parts* -p

すると、cairo_sub_parts_favorite, cairo_sub_parts_retweetで宣言されているGdk::SubPartsFavorite, Gdk::SubPartsRetweetは両方ともGdk::SubPartsVoterを継承しています。
Gdk::SubPartsVoterはその名の通りcairo_sub_parts_voter.rbで宣言されているので、それを見れば良さそうです。
initializeあたりが参考になりそうですね。

##### file: core/mui/cairo_sub_parts_voter.rb
....
  def initialize(*args)
    super
    @icon_width, @icon_height, @margin, @votes, @user_icon = 24, 24, 2, get_default_votes.to_a, Hash.new
    @avatar_rect = []
    @icon_ofst = 0
    helper.ssc(:click){ |this, e, x, y|
      ofsty = helper.mainpart_height
      helper.subparts.each{ |part|
        break if part == self
        ofsty += part.height }
      if ofsty <= y and (ofsty + height) >= y
        case e.button
        when 1
          if(x >= @icon_ofst)
            index = @avatar_rect.bsearch_first {|range| range.include?(x) ? 0 : range.first <=> x}
            user = get_user_by_point(x)
            if user
              Plugin.call(:show_profile, Service.primary, user) end end end end
      false }
    ....
  end
....

参考になりそうなとこだけ抜き出してみました。
helper.ssc(:click){ ...... なあたりが怪しそうですね。

helper.subparts.eachのブロックで、ofstyにheightを追加していっているようです。
そして、後ろのif文でクリックした座標が表示されているアイコンの中に収まってるか確認している感じでしょうね。
case文は、マウスのボタンでしょう(1なら左クリックと見た!)

って感じで、add_linkメソッドを追加してrenderで呼び出してます。

Plugin.create :sub_parts_client do
  # ツイートが投稿されるのに使われたクライアントアプリケーションの名前をTL上に表示する
  class Gdk::SubPartsClient < Gdk::SubParts

    ....

    def add_link(x_size)
      return if @hasLink
      @hasLink = true
      helper.ssc(:click) do |this, e, x, y|
        ofsty = helper.mainpart_height
        helper.subparts.each do |part|
          break if part == self
          ofsty += part.height
        end
        if ( ofsty <= y and y <= (ofsty + height) ) and
           ( (width - x_size - @margin * 2) <= x ) then
          Gtk::openurl(message[:source_url]) if e.button == 1
        end
      end
    end

  end
end

@hasLinkとか気持ち悪くなってるのは、複数回add_linkが実行されるとよろしくない感じだからです。
initializeからadd_linkを呼び出せればいいんでしょうけど、それが難しそうだったので…


そんな感じに参考に書いた感じです。
後半ブログ書くのめんどくさくなっててきとーである。うん。

mikutter本体にパッチをあてないとどうにしろ無理っぽい感じなのでぐぬぬって感じです。
モンキーパッチな感じでやるのも考えたのですが、うまくいかないしいいやって思って諦めました。