File: message.rb

package info (click to toggle)
mikutter 3.0.7%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 9,396 kB
  • ctags: 1,916
  • sloc: ruby: 16,619; sh: 117; makefile: 27
file content (473 lines) | stat: -rw-r--r-- 16,251 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
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# -*- coding:utf-8 -*-

miquire :core, 'user'
miquire :core, 'retriever'
miquire :core, 'messageconverters'

require 'net/http'
require 'delegate'
miquire :lib, 'typed-array', 'timelimitedqueue'

=begin
= Message
投稿1つを表すクラス。
=end
class Message < Retriever::Model
  # screen nameにマッチする正規表現
  MentionMatcher      = /(?:@|@|〄|☯|⑨|♨|(?:\W|^)D )([a-zA-Z0-9_]+)/.freeze

  # screen nameのみから構成される文字列から、@などを切り取るための正規表現
  MentionExactMatcher = /\A(?:@|@|〄|☯|⑨|♨|D )?([a-zA-Z0-9_]+)\Z/.freeze

  @@system_id = 0
  @@appear_queue = TimeLimitedQueue.new(65536, 0.1, Set){ |messages|
    Plugin.call(:appear, messages) }

  # args format
  # key     | value(class)
  #---------+--------------
  # id      | id of status(mixed)
  # entity  | entity(mixed)
  # message | posted text(String)
  # tags    | kind of message(Array)
  # user    | user who post this message(User or Hash or mixed(User IDNumber))
  # reciver | recive user(User)
  # replyto | source message(Message or mixed(Status ID))
  # retweet | retweet to this message(Message or StatusID)
  # post    | post object(Service)
  # image   | image(URL or Image object)

  self.keys = [[:id, :int, true],         # ID
               [:message, :string, true], # Message description
               [:user, User, true],       # Send by user
               [:receiver, User],         # Send to user
               [:replyto, Message],       # Reply to this message
               [:retweet, Message],       # ReTweet to this message
               [:source, :string],        # using client
               [:geo, :string],           # geotag
               [:exact, :bool],           # true if complete data
               [:created, :time],         # posted time
               [:modified, :time],        # updated time
             ]

  # appearイベント
  def self.appear(message) # :nodoc:
    @@appear_queue.push(message)
  end

  # Message.newで新しいインスタンスを作らないこと。インスタンスはコアが必要に応じて作る。
  # 検索などをしたい場合は、 _Retriever_ のメソッドを使うこと
  def initialize(value)
    type_strict value => Hash
    value.update(system) if value[:system]
    if not(value[:image].is_a?(Message::Image)) and value[:image]
      value[:image] = Message::Image.new(value[:image]) end
    super(value)
    if self[:replyto].is_a? Message
      self[:replyto].add_child(self) end
    if self[:retweet].is_a? Message
      self[:retweet].add_child(self) end
    @entity = Entity.new(self)
    Message.appear(self)
  end

  # 投稿主のidnameを返す
  def idname
    user[:idname]
  end

  # この投稿へのリプライをつぶやく
  def post(other, &proc)
    other[:replyto] = self
    other[:receiver] = self[:user]
    service = Service.primary
    if service.is_a? Service
      service.post(other){|*a| yield(*a) if block_given? } end end

  # リツイートする
  def retweet
    service = Service.primary
    if retweetable? and service
      service.retweet(self){|*a| yield(*a) if block_given? } end end

  # この投稿を削除する
  def destroy
    service = Service.primary
    if deletable? and service
      service.destroy(self){|*a| yield(*a) if block_given? } end end

  # お気に入り状態を変更する。_fav_ がtrueならお気に入りにし、falseならお気に入りから外す。
  def favorite(fav = true)
    service = Service.primary
    if favoritable? and service
      service.favorite(self, fav) end end

  # お気に入りから削除する
  def unfavorite
    favorite(false) end

  # この投稿のお気に入り状態を返す。お気に入り状態だった場合にtrueを返す
  def favorite?
    favorited_by.include?(Service.primary!.user_obj)
  rescue Service::NotExistError
    false end

  # 投稿がシステムメッセージだった場合にtrueを返す
  def system?
    self[:system]
  end

  # この投稿にリプライする権限があればtrueを返す
  def repliable?
    !!Service.primary end

  # この投稿をお気に入りに追加する権限があればtrueを返す
  def favoritable?
    Service.primary and not(system?) end
  alias favoriable? favoritable?

  # この投稿をリツイートする権限があればtrueを返す
  def retweetable?
    Service.primary and not system? and not from_me? and not user[:protected] end

  # この投稿を削除する権限があればtrueを返す
  def deletable?
    from_me? end

  # この投稿の投稿主のアカウントの全権限を所有していればtrueを返す
  def from_me?
    return false if system?
    Service.map(&:user_obj).include?(self[:user]) end

  # この投稿が自分宛ならばtrueを返す
  def to_me?
    system? or Service.map(&:user_obj).find(&method(:receive_to?)) end

  # この投稿の投稿主を返す
  def user
    self.get(:user, -1) end

  # この投稿のServiceオブジェクトを返す。
  # 設定されてなければnilを返す
  def service
    warn "Message#service is obsolete method. use `Service.primary'."
    Service.primary end

  # この投稿を宛てられたユーザを返す
  def receiver
    if self[:receiver].is_a? User
      self[:receiver]
    elsif self[:receiver]
      receiver_id = self[:receiver]
      self[:receiver] = parallel{
        self[:receiver] = User.findbyid(receiver_id) }
    else
      match = MentionMatcher.match(self[:message].to_s)
      if match
        result = User.findbyidname(match[1])
        self[:receiver] = result if result end end end

  # ユーザ _other_ に宛てられたメッセージならtrueを返す。
  # _other_ は、 User か_other_[:id]と_other_[:idname]が呼び出し可能なもの。
  def receive_to?(other)
    type_strict other => :[]
    (self[:receiver].is_a?(User) and other[:id] == self[:receiver][:id]) or receive_user_screen_names.include? other[:idname] end

  # このツイートが宛てられたユーザを可能な限り推測して、その idname(screen_name) を配列で返す。
  # 例えばツイート本文内に「@a @b @c」などと書かれていたら、["a", "b", "c"]を返す。
  # ==== Return
  # 宛てられたユーザの idname(screen_name) の配列
  def receive_user_screen_names
    self[:message].to_s.scan(MentionMatcher).map(&:first) end

  # 自分がこのMessageにリプライを返していればtrue
  def mentioned_by_me?
    children.any?{ |m| m.from_me? } end

  # このメッセージが何かしらの別のメッセージに宛てられたものなら真
  def has_receive_message?
    self[:replyto] end

  # このメッセージが何かに対するリツイートなら真
  def retweet?
    !!self[:retweet] end

  # この投稿が別の投稿に宛てられたものならそれを返す。
  # _force_retrieve_ がtrueなら、呼び出し元のスレッドでサーバに問い合わせるので、
  # 親投稿を受信していなくてもこの時受信できるが、スレッドがブロッキングされる。
  # falseならサーバに問い合わせずに結果を返す。
  # Messageのインスタンスかnilを返す。
  def receive_message(force_retrieve=false)
    replyto_source(force_retrieve) or retweet_source(force_retrieve) end

  def receive_message_d(force_retrieve=false)
    Thread.new{ receive_message(force_retrieve) } end

  def self.define_source_getter(key, condition=ret_nth, &onfound)
    define_method("#{key}_source"){ |*args|
      force_retrieve = args.first
      if(condition === self[:message].to_s)
        result = get(key, (force_retrieve ? -1 : 1))
        if result.is_a?(Message)
          onfound.call(self, result)
          result end end }
    define_method("#{key}_source_d"){ |*args|
      Thread.new{ __send__("#{key}_source", *args) } } end

  # Message#receive_message と同じ。ただし、リプライ元のみをさがす。
  define_source_getter(:replyto, /@[a-zA-Z0-9_]/){ |this, result|
    result.add_child(this) unless result.children.include?(this) }

  # Message#receive_message と同じ。ただし、ReTweetedのみをさがす。
  define_source_getter(:retweet, -> t { t.start_with? 'RT'.freeze }){ |this, result|
    result.add_child(this) unless result.retweeted_statuses.include?(this) }

  # 投稿の宛先になっている投稿を再帰的にさかのぼり、それぞれを引数に取って
  # ブロックが呼ばれる。
  # _force_retrieve_ は、 Message#receive_message の引数にそのまま渡される
  def each_ancestors(force_retrieve=false, &proc)
    proc.call(self)
    parent = receive_message(force_retrieve)
    parent.each_ancestors(force_retrieve, &proc) if parent
  end

  # 投稿の宛先になっている投稿を再帰的にさかのぼり、それらを配列にして返す。
  # 配列インデックスが大きいものほど、早く投稿された投稿になる。
  # ([0]は[1]へのリプライ)
  def ancestors(force_retrieve=false)
    parent = receive_message(force_retrieve)
    return [self, *parent.ancestors(force_retrieve)] if parent
    [self] end
  memoize :ancestors

  # 投稿の宛先になっている投稿を再帰的にさかのぼり、何にも宛てられていない投稿を返す。
  # つまり、一番祖先を返す。
  def ancestor(force_retrieve=false)
    ancestors(force_retrieve).last end

  # このMessageが属する親子ツリーに属する全てのMessageを含むSetを返す
  # ==== Args
  # [force_retrieve] 外部サーバに問い合わせる場合真
  # ==== Return
  # 関係する全てのツイート(Set)
  def around(force_retrieve = false)
    ancestor(force_retrieve).children_all end

  # この投稿に宛てられた投稿をSetオブジェクトにまとめて返す。
  def children
    @children ||= Plugin.filtering(:replied_by, self, Set.new())[1] + retweeted_statuses end

  # childrenを再帰的に遡り全てのMessageを返す
  # ==== Return
  # このMessageの子全てをSetにまとめたもの
  def children_all
    children.inject(Messages.new([self])){ |result, item| result.concat item.children_all } end

  # この投稿をお気に入りに登録したUserをSetオブジェクトにまとめて返す。
  def favorited_by
    @favorited ||= Plugin.filtering(:favorited_by, self, Set.new())[1] end

  # この投稿を「自分」がふぁぼっていれば真
  def favorited_by_me?(me = Service.services)
    case me
    when Service
      favorited_by.include? me.user_obj
    when Enumerable
      not (Set.new(favorited_by.map(&:idname)) & Set.new(me.map(&:idname))).empty?
    else
      raise ArgumentError, "first argument should be `Service' or `Enumerable'. but given `#{me.class}'" end end

  # この投稿をリツイートしたユーザを返す
  def retweeted_by
    retweeted_statuses.map{ |x| x.user }.uniq
  end

  # この投稿に対するリツイートを返す
  def retweeted_statuses
    @retweets ||= Plugin.filtering(:retweeted_by, self, Set.new)[1].select(&ret_nth) end

  # 選択されているユーザがこのツイートをリツイートしているなら真
  def retweeted?
    retweeted_by.include?(Service.primary!.user_obj)
  rescue Service::NotExistError
    false end

  # この投稿を「自分」がリツイートしていれば真
  def retweeted_by_me?(me = Service.services)
    case me
    when Service
      retweeted_by.include? me.user_obj
    when Enumerable
      not (Set.new(retweeted_by.map(&:idname)) & Set.new(me.map(&:idname))).empty?
    else
      raise ArgumentError, "first argument should be `Service' or `Enumerable'. but given `#{me.class}'" end end

  # 非公式リツイートやハッシュタグを適切に組み合わせて投稿する
  def body
    self[:message].to_s.freeze
  end

  # リンクを貼る場所とその種類を表現するEntityオブジェクトを返す
  def links
    @entity end
  alias :entity :links

  def inspect
    @value.inspect
  end

  # Message#body と同じだが、投稿制限文字数を超えていた場合には、収まるように末尾を捨てる。
  def to_s
    body[0,140].freeze end
  memoize :to_s

  def to_i
    self[:id].to_i end

  # selfを返す
  def to_message
    self end
  alias :message :to_message

  # 本文を人間に読みやすい文字列に変換する
  def to_show
    @to_show ||= body.gsub(/&(gt|lt|quot|amp);/){|m| {'gt' => '>', 'lt' => '<', 'quot' => '"', 'amp' => '&'}[$1] }.freeze end

  # このMessageのパーマリンクを取得する
  # ==== Return
  # パーマリンクのURL(String)か、存在しない場合はnil
  def parma_link
    if not system?
      @parma_link ||= "https://twitter.com/#{user[:idname]}/status/#{self[:id]}".freeze end end

  # :nodoc:
  def marshal_dump
    raise RuntimeError, 'Message cannot marshalize'
  end

  # :nodoc:
  def add_favorited_by(user, time=Time.now)
    type_strict user => User, time => Time
    service = Service.primary
    if service
      set_modified(time) if UserConfig[:favorited_by_anyone_age] and (UserConfig[:favorited_by_myself_age] or service.user != user.idname)
      favorited_by.add(user)
      Plugin.call(:favorite, service, user, self) end end

  # :nodoc:
  def remove_favorited_by(user)
    type_strict user => User
    service = Service.primary
    if service
      favorited_by.delete(user)
      Plugin.call(:unfavorite, service, user, self) end end

  # :nodoc:
  def add_child(child)
    type_strict child => Message
    if child[:retweet]
      if defined? @retweets
        add_retweet_in_this_thread(child)
      else
        SerialThread.new{
          retweeted_by
          add_retweet_in_this_thread(child) } end
    else
      if defined? @children
        add_child_in_this_thread(child)
      else
        SerialThread.new{
          children
          add_child_in_this_thread(child) } end end end

  # 最終更新日時を取得する
  def modified
    @value[:modified] ||= [self[:created], *(defined?(@retweets) ? @retweets : []).map{ |x| x.modified }].select(&ret_nth).max
  end

  private

  def add_retweet_in_this_thread(child)
    type_strict child => Message
    @retweets = [] if not defined? @retweets
    @retweets << child
    service = Service.primary
    set_modified(child[:created]) if service and UserConfig[:retweeted_by_anyone_age] and ((UserConfig[:retweeted_by_myself_age] or service.user != child.user.idname)) end

  def add_child_in_this_thread(child)
    @children << child
  end

  def set_modified(time)
    if modified < time
      self[:modified] = time
      Plugin::call(:message_modified, self) end
    self end

  def system
    { :id => @@system_id += 1,
      :user => User.system,
      :created => Time.now } end

  #
  # Sub classes
  #

  # このツイートのユーザ情報
  class MessageUser < User
    undef_method *(public_instance_methods - [:object_id, :__send__])

    def initialize(user, raw)
      abort if not user.is_a? User
      @raw = raw.freeze
      @user = user end

    def [](key)
      @raw.has_key?(key.to_sym) ? @raw[key.to_sym] : @user[key] end

    def method_missing(*args)
      @user.__send__(*args) end end

  # 添付画像
  class Image
    attr_accessor :url
    attr_reader :resource

    IS_URL = /\Ahttps?:\/\//

    def initialize(resource)
      if(not resource.is_a?(IO)) and (FileTest.exist?(resource.to_s)) then
        @resource = open(resource)
      else
        @resource = resource
        if((IS_URL === resource) != nil) then
          @url = resource
        end
      end
    end

    def path
      if(@resource.is_a?(File)) then
        return @resource.path
      end
      return @url
    end
  end

  # 例外を引き起こした原因となるMessageをセットにして例外を発生させることができる
  class MessageError < Retriever::RetrieverError
    # messageは、Exceptionクラスと名前が被る
    attr_reader :to_message

    def initialize(body, message)
      super("#{body} occured by #{message[:id]}(#{message[:message]})")
      @to_message = message end

  end

end

class Messages < TypedArray(Message)
end

miquire :core, 'entity'