File: plugin.rb

package info (click to toggle)
mikutter 3.5.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 10,256 kB
  • ctags: 2,165
  • sloc: ruby: 19,079; sh: 205; makefile: 20
file content (401 lines) | stat: -rw-r--r-- 15,196 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# -*- coding: utf-8 -*-
# Plugin
#

miquire :core, 'configloader'
miquire :core, 'environment'
miquire :core, 'delayer'

require 'monitor'
require 'set'
require 'thread'

#
#= Plugin プラグイン管理/イベント管理モジュール
#
# CHIコアにプラグインを報告します。
# Plugin.create でPluginTagのインスタンスを作り、コアにプラグインを登録します。
# イベントリスナーの登録とイベントの発行については、Plugin::PluginTagを参照してください。
#
#== プラグインの実行順序
# まず、Plugin.call()が呼ばれると、予めadd_event_filter()で登録されたフィルタ関数に
# 引数が順次通され、最終的な戻り値がadd_event()に渡される。イメージとしては、
# イベントリスナ(*フィルタ(*引数))というかんじ。
# リスナもフィルタも、実行される順序は特に規定されていない。
module Plugin
  extend Gem::Deprecate

  @@eventqueue = Queue.new

  Thread.new{
    while proc = @@eventqueue.pop
      proc.call end
  }

  def self.gen_event_ring
    Hash.new{ |hash, key| hash[key] = [] }
  end
  @@event          = gen_event_ring # { event_name => [[plugintag, proc]] }
  @@add_event_hook = gen_event_ring
  @@event_filter   = gen_event_ring

  # イベントリスナーを追加する。
  def self.add_event(event_name, tag, &callback)
    @@event[event_name.to_sym] << [tag, callback]
    call_add_event_hook(event_name, callback)
    callback end

  # イベントフィルタを追加する。
  # フィルタは、イベントリスナーと同じ引数で呼ばれるし、引数の数と同じ数の値を
  # 返さなければいけない。
  def self.add_event_filter(event_name, tag, &callback)
    @@event_filter[event_name.to_sym] << [tag, callback]
    callback end

  def self.fetch_event(event_name, tag, &callback)
    call_add_event_hook(event_name, callback)
    callback end

  def self.add_event_hook(event_name, tag, &callback)
    @@add_event_hook[event_name.to_sym] << [tag, callback]
    callback end

  def self.detach(event_name, event)
    deleter = lambda{|events| events[event_name.to_sym].reject!{ |e| e[1] == event } }
    deleter.call(@@event) or deleter.call(@@event_filter) or deleter.call(@@add_event_hook) end

  # フィルタ関数を用いて引数をフィルタリングする
  def self.filtering(event_name, *args)
    length = args.size
    catch(:filter_exit){
      @@event_filter[event_name.to_sym].inject(args){ |store, plugin|
        result = store
        plugintag, proc = *plugin
        boot_plugin(plugintag, event_name, :filter, false){
          result = proc.call(*store){ |result| throw(:filter_exit, result) }
          if length != result.size
            raise "filter changes arguments length (#{length} to #{result.size})" end
          result } } } end

  # イベント _event_name_ を呼ぶ予約をする。第二引数以降がイベントの引数として渡される。
  # 実際には、これが呼ばれたあと、することがなくなってから呼ばれるので注意。
  def self.call(event_name, *args)
    SerialThread.new{
      plugin_callback_loop(@@event, event_name, :proc, *filtering(event_name, *args)) } end

  # イベントが追加されたときに呼ばれるフックを呼ぶ。
  # _callback_ には、登録されたイベントのProcオブジェクトを渡す
  def self.call_add_event_hook(event_name, callback)
    plugin_callback_loop(@@add_event_hook, event_name, :hook, callback) end

  # plugin_loopの簡略化版。プラグインに引数 _args_ をそのまま渡して呼び出す
  def self.plugin_callback_loop(ary, event_name, kind, *args)
    plugin_loop(ary, event_name, kind){ |tag, proc|
      proc.call(*args){ throw(:plugin_exit) } } end

  # _ary_ [ _event\_name_ ] に登録されているプラグイン一つひとつを引数に _proc_ を繰り返し呼ぶ。
  # _proc_ のシグニチャは以下の通り。
  #   _proc_ ( プラグイン名, コールバック )
  def self.plugin_loop(ary, event_name, kind, &proc)
    ary[event_name.to_sym].each{ |plugin|
      boot_plugin(plugin.first, event_name, kind){
         proc.call(*plugin) } } end

  # プラグインを起動できるならyieldする。コールバックに引数は渡されない。
  def self.boot_plugin(plugintag, event_name, kind, delay = true, &routine)
    if(plugintag.active?)
      if(delay)
        Delayer.new{ call_routine(plugintag, event_name, kind, &routine) }
      else
        call_routine(plugintag, event_name, kind, &routine) end end end

  # プラグインタグをなければ作成して返す。
  def self.create(name)
    PluginTag.create(name) end

  # ブロックの実行時間を記録しながら実行
  def self.call_routine(plugintag, event_name, kind)
    catch(:plugin_exit){ yield } end
    # begin
    #   yield
    # rescue Exception => e
    #   plugin_fault(plugintag, event_name, kind, e) end

  # 登録済みプラグインの一覧を返す。
  # 返すHashは以下のような構造。
  #  { plugin tag =>{
  #      event name => [proc]
  #    }
  #  }
  # plugin tag:: Plugin::PluginTag のインスタンス
  # event name:: イベント名。Symbol
  # proc:: イベント発生時にコールバックされる Proc オブジェクト。
  def self.plugins
    result = Hash.new{ |hash, key|
      hash[key] = Hash.new{ |hash, key|
        hash[key] = [] } }
    @@event.each_pair{ |event, pair|
      result[pair[0]][event] << proc }
    result
  end

  # 登録済みプラグイン名を一次元配列で返す
  def self.plugin_list
    Plugin::PluginTag.plugins end

  # プラグイン処理中に例外が発生した場合、アプリケーションごと落とすかどうかを返す。
  # trueならば、その場でバックトレースを吐いて落ちる、falseならエラーを表示してプラグインをstopする
  def self.abort_on_exception?
    true end

  def self.plugin_fault(plugintag, event_name, event_kind, e)
    error e
    if abort_on_exception?
      abort
    else
      Plugin.call(:update, nil, [Message.new(:message => "プラグイン #{plugintag} が#{event_kind} #{event_name} 処理中にクラッシュしました。プラグインの動作を停止します。\n#{e.to_s}",
                                             :system => true)])
      plugintag.stop! end end
end

=begin rdoc

= Plugin プラグインタグクラス

 プラグインを一意に識別するためのタグ。
 newは使わずに、 Plugin.create でインスタンスを作ること。

== イベントの種類

 以下に、監視できる主なイベントを示す。

=== boot(Post service)
起動時に、どのイベントよりも先に一度だけ呼ばれる。

=== period(Post service)
毎分呼ばれる。必ず60秒ごとになる保証はない。

=== update(Post service, Array messages)
フレンドタイムラインが更新されたら呼ばれる。ひとつのつぶやきはかならず1度しか引数に取られず、
_messages_ には同時に複数の Message のインスタンスが渡される(ただし、削除された場合は削除フラグを
立てて同じつぶやきが流れる)。

=== mention(Post service, Array messages)
updateと同じ。ただし、自分宛のリプライが来たときに呼ばれる点が異なる。

=== posted(Post service, Array messages)
自分が投稿したメッセージ。

=== appear(Array messages)
updateと同じ。ただし、タイムライン、検索結果、リスト等、受信したすべてのつぶやきを対象にしている。

=== message_modified(Message message)
messageの内容が変わったときに呼ばれる。
おもに、ふぁぼられ数やRT数が変わったときに呼ばれる。

=== list_data(Post service, Array ulist)
フォローしているリスト一覧に変更があれば呼ばれる。なお、このイベントにリスナーを登録すると、すぐに
現在フォローしているリスト一覧を引数にコールバックが呼ばれる。

=== list_created(Post service, Array ulist)
新しくリストが作成されると、それを引数に呼ばれる。

=== list_destroy(Post service, Array ulist)
リストが削除されると、それを引数に呼ばれる。

=== mui_tab_regist(Gtk::Widget container, String label, String image=nil)
ウィンドウにタブを追加する。 _label_ はウィンドウ内での識別名にも使われるので一意であること。
_image_ は画像への相対パスかURLで、通常は #MUI::Skin.get の戻り値を使う。
_image_ が省略された場合は、 _label_ が使われる。

=== mui_tab_remove(String label)
ラベル _label_ をもつタブを削除する。

=== mui_tab_active(String label)
ラベル _label_ のついたタブをアクティブにする。

=== apilimit(Time time)
サーバによって、時間 _time_ までクエリの実行を停止された時に呼ばれる。

=== apifail(String text)
何らかのクエリが実行に失敗した場合に呼ばれる。サーバからエラーメッセージが帰ってきた場合は
_text_ に渡される。エラーメッセージが得られなかった場合はnilが渡される。

=== apiremain(Integer remain, Time expire, String transaction)
サーバへのクリエ発行が時間 _expire_ までに _remain_ 回実行できることを通知するために呼ばれる。
現在のTwitterの仕様では、クエリを発行するたびにこれが呼ばれる。

=== ipapiremain(Integer remain, Time expire, String transaction)
基本的にはapiremainと同じだが、IPアドレス規制について動くことが違う。

=== rewindstatus(String mes)
ユーザに情報 _mes_ を「さりげなく」提示する。 GUI プラグインがハンドルしていて、ステータスバーを
更新する。

=== retweet(Array messages)
リツイートを受信したときに呼ばれる

=== favorite(Post service, User user, Message message)
_user_ が _message_ をお気に入りに追加した時に呼ばれる。

=== unfavorite(Post service, User user, Message message)
_user_ が _message_ をお気に入りから外した時に呼ばれる。

=== after_event(Post service)
periodなど、毎分実行されるイベントのクロールが終わった後に呼び出される。

=== play_sound(String filename)
ファイル名 _filename_ の音楽ファイルを鳴らす。

=== popup_notify(User user, String text)
通知を表示する。雰囲気としては、
- Windows : バルーン
- Linux : libnotify
- Mac : Growl
みたいなイメージの通知。 _user_ のアイコンが使われ、名前がタイトルになり、本文は _text_ が使われる。

=== query_start(:serial => Integer, :method => Symbol|String, :path => String, :options => Hash, :start_time => Time)
HTTP問い合わせが始まった時に呼ばれる。
serial::
  コネクションのID
method::
  HTTPメソッド名。GETやPOSTなど
path::
  サーバ上のパス。/statuses/show.json など
options::
  雑多な呼び出しオプション。
start_time::
  クエリの開始時間

=== query_end(:serial => Integer, :method => Symbol|String, :path => String, :options => Hash, :start_time => Time, :end_time => Time, :res => Net::HTTPResponse|Exception)
HTTP問い合わせが終わった時に呼ばれる。
serial::
  コネクションのID
method::
  HTTPメソッド名。GETやPOSTなど
path::
  サーバ上のパス。/statuses/show.json など
options::
  雑多な呼び出しオプション。
start_time::
  クエリの開始時間
end_time::
  クエリのレスポンスを受け取った時間。
res::
  受け取ったレスポンス。通常はNet::HTTPResponseを渡す。捕捉できない例外が発生した場合はここにその例外を渡す。

== フィルタ

以下に、フックできる主なフィルタを示す。

=== favorited_by(Message message, Set users)
_message_ をお気に入りに入れているユーザを取得するためのフック。
_users_ は、お気に入りに入れているユーザの集合。

=== show_filter(Enumerable messages)
_messages_ から、表示してはいけないものを取り除く

=end
class Plugin::PluginTag

  include ConfigLoader

  @@plugins = [] # plugin

  attr_reader :name
  alias to_s name

  def initialize(name = :anonymous)
    @name = name
    active!
    register end

  # 新しくプラグインを作成する。もしすでに同じ名前で作成されていれば、新しく作成せずにそれを返す。
  def self.create(name)
    plugin = @@plugins.find{ |p| p.name == name }
    if plugin
      plugin
    else
      Plugin::PluginTag.new(name) end end

  def self.plugins
    @@plugins
  end

  # イベント _event_name_ を監視するイベントリスナーを追加する。
  def add_event(event_name, &callback)
    Plugin.add_event(event_name, self, &callback)
  end

  # イベントフィルタを設定する。
  # フィルタが存在した場合、イベントが呼ばれる前にイベントフィルタに引数が渡され、戻り値の
  # 配列がそのまま引数としてイベントに渡される。
  # フィルタは渡された引数と同じ長さの配列を返さなければいけない。
  def add_event_filter(event_name, &callback)
    Plugin.add_event_filter(event_name, self, &callback)
  end

  def fetch_event(event_name, &callback)
    Plugin.fetch_event(event_name, self, &callback)
  end

  # イベント _event_name_ にイベントが追加されたときに呼ばれる関数を登録する。
  def add_event_hook(event_name, &callback)
    Plugin.add_event_hook(event_name, self, &callback)
  end

  # イベントの監視をやめる。引数 _event_ には、add_event, add_event_filter, add_event_hook の
  # いずれかの戻り値を与える。
  def detach(event_name, event)
    Plugin.detach(event_name, event)
  end

  def at(key, ifnone=nil)
    super("#{@name}_#{key}".to_sym, ifnone) end

  def store(key, val)
    super("#{@name}_#{key}".to_sym, val) end

  def stop!
    @status = :stop end

  def stop?
    @status == :stop end

  def active!
    @status = :active end

  def active?
    @status == :active end

  private

  def register
    @@plugins.push(self) end
  alias :regist :register
  deprecate :regist, "register", 2016, 12

end

Module.new do
  def self.gen_never_message_filter
    appeared = Set.new
    lambda{ |service, messages|
      [service,
       messages.select{ |m|
         appeared.add(m[:id].to_i) if m and not(appeared.include?(m[:id].to_i)) }] } end

  def self.never_message_filter(event_name, *other)
    Plugin.create(:core).add_event_filter(event_name, &gen_never_message_filter)
    never_message_filter(*other) unless other.empty?
  end

  never_message_filter(:update, :mention)

  Plugin.create(:core).add_event(:appear){ |messages|
    retweets = messages.select(&:retweet?)
    if not(retweets.empty?)
      Plugin.call(:retweet, retweets) end }
end

miquire :plugin # if defined? Test::Unit