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 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599
|
# coding: utf-8
module Plugin::Mastodon
# https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md#status
# 必ずStatus.buildメソッドを通して生成すること
class Status < Diva::Model
extend Gem::Deprecate
include Diva::Model::MessageMixin
register :mastodon_status, name: Plugin[:mastodon]._('トゥート'), timeline: true, reply: true, myself: true
field.string :id, required: true
field.string :original_uri, required: true # APIから取得するfediverse uniqueなURI文字列
field.uri :url, required: true
field.has :account, Account, required: true
field.string :in_reply_to_id
field.string :in_reply_to_account_id
field.has :reblog, Status
field.string :content, required: true
field.time :created_at, required: true
field.time :modified
field.time :created
field.int :reblogs_count
field.int :favourites_count
field.bool :reblogged
field.bool :favourited
field.bool :muted
field.bool :sensitive
field.string :spoiler_text
field.string :visibility
field.has :application, Application
field.string :language
field.bool :pinned
field.bool :bookmarked
field.string :domain, required: true # APIには無い追加フィールド
field.has :emojis, [Emoji]
field.has :media_attachments, [Attachment]
field.has :mentions, [Mention]
field.has :tags, [Tag]
field.has :card, Card
field.has :poll, Poll
attr_accessor :reblog_status_uris # :: [String] APIには無い追加フィールド
# ブーストしたStatusのuri(これらはreblogフィールドの値としてこのオブジェクトを持つ)と、acctを保持する。
attr_accessor :favorite_accts # :: [String] APIには無い追加フィールド
attr_accessor :description
attr_accessor :score
alias :uri :url # mikutter側の都合で、URI.parse可能である必要がある(API仕様上のuriフィールドとは異なる)。
alias :perma_link :url
alias :muted? :muted
alias :pinned? :pinned
alias :bookmarked? :bookmarked
alias :retweet_ancestor :reblog
alias :sensitive? :sensitive # NSFW系プラグイン用
@@mute_mutex = Thread::Mutex.new
VALID_URL_PRECEDING_CHARS = /(?:[^A-Z0-9@@$##]|^)/io
VALID_URL_QUERY_CHARS = /[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|@]/i
VALID_URL_QUERY_ENDING_CHARS = /[a-z0-9_&=#\/\-]/i
URL_PATTERN = %r{
(#{VALID_URL_PRECEDING_CHARS})
(
(https?:\/\/)
((?:(?!-)[a-z0-9-]{1,63}(?<!-)\.)*(?:xn--)?[a-z]{2,})
(?::(\d{1,5}))?
(\/
[^\s#?!$&'\*,;=]*
(?:\#[^\s#?!$&'\*,;=]*)?
)?
(\?#{VALID_URL_QUERY_CHARS}*#{VALID_URL_QUERY_ENDING_CHARS})?
)
}iox
TOOT_URI_RE = %r!\Ahttps://([^/]+)/@\w{1,30}/(\d+)\z!.freeze
TOOT_ACTIVITY_URI_RE = %r!\Ahttps://(?<domain>[^/]*)/users/(?<acct>[^/]*)/statuses/(?<status_id>[^/]*)/activity\z!.freeze
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[[:word:]]+)?)/i
handle TOOT_URI_RE do |uri|
Status.findbyuri(uri) || Status.fetch(uri)
end
class << self
@@status_storage = WeakStorage.new(String, Status, name: 'toot')
def add_status_storage(toot)
raise "uri must not nil. but given #{toot.uri.inspect}" unless toot.uri
if @@status_storage.has_key?("#{toot.domain}:#{toot.uri}")
warn "key already exists: #{toot.domain}:#{toot.uri}"
end
@@status_storage["#{toot.domain}:#{toot.uri}"] = toot
end
# urlで検索する。
# 但しブーストの場合はfediverse uri
def findbyuri(uri, domain: nil)
domain ||= Diva::URI(uri).host
@@status_storage["#{domain}:#{uri}"]
end
def add_mutes(account_hashes)
@@mute_mutex.synchronize {
@@mutes ||= []
@@mutes += account_hashes.map do |hash|
hash = Account.regularize_acct hash
hash[:acct]
end
@@mutes = @@mutes.uniq
}
end
def clear_mutes
@@mute_mutex.synchronize {
@@mutes = []
}
end
def muted?(acct)
@@mute_mutex.synchronize {
@@mutes.any? { |a| a == acct }
}
end
# Mastodon APIレスポンスのJSONをパースした結果のHashを受け取り、それに対応する
# インスタンスを返す。
# ==== Args
# [server] Mastodonサーバ (Plugin::Mastodon::Instance)
# [record] APIレスポンスのstatusオブジェクト (Hash)
# ==== Return
# Plugin::Mastodon::Status
def build(server, record)
json2status(server.domain, record)
end
# buildと似ているが、配列を受け取って複数のインスタンスを同時に取得できる。
# ==== Args
# [server] Mastodonサーバ (Plugin::Mastodon::Instance)
# [json] APIレスポンスのstatusオブジェクトの配列 (Array)
# ==== Return
# Plugin::Mastodon::Status の配列
def bulk_build(server, json)
json.map do |record|
build(server, record)
end.to_a
end
def json2status(domain_name, record)
record[:domain] = domain_name
is_boost = false
if record[:reblog]
is_boost = true
boost_record = Plugin::Mastodon::Util.deep_dup(record)
boost_record[:reblog] = nil
record = record[:reblog]
record[:domain] = domain_name
end
uri = record[:url] # quoting_messages等のために@@status_storageには:urlで入れておく
status = merge_or_create(domain_name, uri, record)
return nil unless status
# ブーストの処理
if !is_boost
status
# ブーストではないので、普通にstatusを返す。
else
boost_uri = boost_record[:uri] # reblogには:urlが無いので:uriで入れておく
boost = merge_or_create(domain_name, boost_uri, {**boost_record, url: boost_uri})
return nil unless boost
status.reblog_status_uris << { uri: boost_uri, acct: boost_record[:account][:acct] }
status.reblog_status_uris.uniq!
# ageなどの対応
status.set_modified(boost.modified) if UserConfig[:retweeted_by_anyone_age] and (UserConfig[:retweeted_by_myself_age] or !boost.account.me?)
boost[:retweet] = boost.reblog = status
# わかりづらいが「ブーストした」statusの'reblog'プロパティにブースト元のstatusを入れている
boost
# 「ブーストした」statusを返す(appearしたのはそれに間違いないので。ブースト元はdon't care。
# Gtk::TimeLine#block_addではmessage.retweet?ならmessage.retweet_sourceを取り出して追加する。
end
end
def merge_or_create(domain_name, uri, new_hash)
@@mutes ||= []
if new_hash[:account] && new_hash[:account][:acct]
account_hash = Account.regularize_acct(new_hash[:account])
if @@mutes.index(account_hash[:acct])
return nil
end
end
status = findbyuri(uri, domain: domain_name)
if status
status = status.merge(domain_name, new_hash)
else
status = Status.new(new_hash)
end
status
end
def fetch(uri)
if m = TOOT_URI_RE.match(uri.to_s)
domain_name = m[1]
id = m[2]
Plugin::Mastodon::API.status(domain_name, id).next{ |resp|
Status.build(Plugin::Mastodon::Instance.load(domain_name), resp.value)
}
else
Delayer::Deferred.new(true).tap{|d| d.fail(nil) }
end
end
end
def initialize(hash)
@reblog_status_uris = []
@favorite_accts = []
if hash[:created_at].is_a? String
# タイムゾーン考慮
hash[:created_at] = Time.parse(hash[:created_at]).localtime
end
# cairo_sub_parts_message_base用
hash[:created] = hash[:created_at]
hash[:modified] = hash[:created_at] unless hash[:modified]
# mikutterはuriをURI型であるとみなす
hash[:original_uri] = hash[:uri]
hash.delete :uri
# sub_parts_client用
if hash[:application] && hash[:application][:name]
hash[:source] = hash[:application][:name]
end
# Mentionのacctにドメイン付加
if hash[:mentions]
hash[:mentions].each_index do |i|
acct = hash[:mentions][i][:acct]
hash[:mentions][i][:acct] = Account.regularize_acct_by_domain(hash[:domain], acct)
end
end
# notification用
hash[:retweet] = hash[:reblog]
super hash
self[:user] = self[:account]
if self.reblog.is_a?(Status) && self.reblog.account.is_a?(Account)
self.reblog[:user] = self.reblog.account
end
@emoji_score = Hash.new
content = actual_status.content
unless spoiler_text.empty?
content = spoiler_text + "<br>----<br>" + content
end
@description, @score = Plugin::Mastodon::Parser.dictate_score(content, mentions: mentions, emojis: emojis, media_attachments: media_attachments, poll: poll)
self.class.add_status_storage(self)
Plugin.call(:mastodon_appear_toots, [self])
end
def inspect
"mastodon-status(#{uri} #{description})"
end
def merge(domain_name, new_hash)
# 取得元が発言者の所属サーバーであれば優先する
account_domain = account&.domain
account_domain2 = Account.domain(new_hash[:account][:url])
if !domain || domain != account_domain && domain_name == account_domain2
self.id = new_hash[:id]
self.domain = domain_name
if !(application && self[:source]) && new_hash[:application]
self.application = Application.new(new_hash[:application])
self[:source] = application.name
end
end
reblogs_count = new_hash[:reblogs_count]
favourites_count = new_hash[:favourites_count]
pinned = new_hash[:pinned]
self
end
def actual_status
reblog || self
end
def icon
actual_status.account.icon
end
def user
account
end
def server
@server ||= Plugin::Mastodon::Instance.load(domain)
end
def retweet_count
[actual_status.reblog_status_uris.size, actual_status.reblogs_count].compact.max
end
def favorite_count
[@favorite_accts.size, actual_status.favourites_count].compact.max
end
def retweet?
reblog.is_a? Status
end
def retweeted_by
actual_status
.reblog_status_uris
.lazy
.filter_map { |v| v[:acct] }
.filter_map(&Account.method(:findbyacct))
.uniq
.to_a
end
def shared?(counterpart=nil)
counterpart ||= Plugin.filtering(:world_current, nil).first
if counterpart.respond_to?(:user_obj)
counterpart = counterpart.user_obj
end
if counterpart.is_a?(Account)
actual_status.retweeted_by.include?(counterpart)
end
end
alias :retweeted? :shared?
def favorited_by
@favorite_accts.lazy.filter_map(&Account.method(:findbyacct)).uniq.to_a
end
def favorite?(counterpart = nil)
counterpart ||= Plugin.filtering(:world_current, nil).first
if counterpart.respond_to?(:user_obj)
counterpart = counterpart.user_obj
end
if counterpart.is_a?(Account)
@favorite_accts.include?(counterpart.idname)
end
end
# sub_parts_client用
def source
actual_status.application&.name
end
def add_attachments(text)
if media_attachments && !media_attachments.empty?
media_attachments.each do |attachment|
url = attachment.text_url || attachment.url
if !text.include?(url.to_s)
text += " <a href=\"#{url}\">#{url}</a>"
end
end
end
text
end
# 自分が _self_ にリプライを返していればtrue
def mentioned_by_me?
children.any?(&:from_me?)
end
# _self_ を宛先としたStatusのリストを返す
def children
@children ||= Set.new(retweeted_statuses)
end
protected def add_child(child)
children << child
end
# この投稿の投稿主のアカウントの全権限を所有していればtrueを返す
def from_me?(world = Plugin.collect(:worlds))
account.me?(world)
end
# 通知用
# 自分へのmention
def mention_to_me?(world)
return false if mentions.empty?
return false if (!world.respond_to?(:account) || !world.account.respond_to?(:acct))
mentions.map{|mention| mention.acct }.include?(world.account.acct)
end
# 自分へのreblog
def reblog_to_me?(world)
return false unless reblog
reblog.from_me?(world)
end
def to_me_world
world = Plugin.filtering(:world_current, nil).first
return nil if (!mention_to_me?(world) && !reblog_to_me?(world))
world
end
def to_me?(world = nil)
if world
if world.is_a? Plugin::Mastodon::World
return mention_to_me?(world) || reblog_to_me?(world)
else
return false
end
end
!!to_me_world
end
# activity用
def to_s
description
end
# ふぁぼ
def favorite(do_fav)
world, = Plugin.filtering(:world_current, nil)
if do_fav
Plugin[:mastodon].favorite(world, self)
else
Plugin[:mastodon].unfavorite(world, self)
end
end
def retweeted_statuses
reblog_status_uris.map{|pair| self.class.findbyuri(pair[:uri]) }.compact
end
alias :retweeted_sources :retweeted_statuses
# Message#.introducer
# 本当はreblogがあればreblogをreblogした最後のStatusを返す
# reblogがなければselfを返す
def introducer(world = nil)
self
end
# 返信スレッド用
def around(force_retrieve=false)
if force_retrieve
resp = Plugin::Mastodon::API.call!(:get, domain, '/api/v1/statuses/' + id + '/context')
return [self] unless resp
@around = [*Status.bulk_build(server, resp[:ancestors]),
self,
*Status.bulk_build(server, resp[:descendants])]
else
@around || [self]
end
end
# tootのリプライ先を再帰的に遡って、見つかった順に列挙する。
# 最初に列挙する要素は常に _self_ で、ある要素は次の要素へのリプライとなっている。
# ==== Args
# [force_retrieve] サーバへの問い合わせを許可するフラグ
# ==== Return
# Enumerator :: リプライツリーを祖先方向に辿って列挙する (Plugin::Mastodon::Status)
def ancestors(force_retrieve=false)
if force_retrieve
@ancestors ||= ancestors_force.freeze
else
[self, *replyto_source&.ancestors].freeze
end
end
private def ancestors_force
resp = Plugin::Mastodon::API.call!(:get, domain, '/api/v1/statuses/' + id + '/context')
if resp
[self, *Status.bulk_build(server, resp[:ancestors]).reverse]
else
[self]
end
end
# 返信表示用
def has_receive_message?
!!in_reply_to_id
end
def repliable?(counterpart=nil)
true
end
# 返信表示用
def replyto_source(force_retrieve=false)
# TODO: サーバ+IDでStatusを保存するWeakStoreを使ってキャッシュしたいわね
@replyto_source ||=
force_retrieve ? replyto_source_force : nil
end
private def replyto_source_force
unless domain
# 何故かreplyviewerに渡されたStatusからdomainが消失することがあるので復元を試みる
world, = Plugin.filtering(:mastodon_current, nil)
if world
# 見つかったworldでstatusを取得し、id, domain, in_reply_to_idを上書きする。
status = Plugin::Mastodon::API.status_by_url!(world.domain, world.access_token, url)
if status
self[:id] = status[:id]
self[:domain] = world.domain
self[:in_reply_to_id] = status[:in_reply_to_id]
if status[:reblog]
self.reblog[:id] = status[:reblog][:id]
self.reblog[:domain] = world.domain
self.reblog[:in_reply_to_id] = status[:reblog][:in_reply_to_id]
end
end
end
end
Plugin::Mastodon::API.status!(domain, in_reply_to_id)
&.yield_self { |r| Status.build(server, r.value) }
&.tap { |parent| parent.add_child(self) }
end
# 返信表示用
def replyto_source_d(force_retrieve=true)
promise = Delayer::Deferred.new(true)
Thread.new do
result = replyto_source(force_retrieve)
if result.is_a? Status
promise.call(result)
else
promise.fail(result)
end
rescue Exception => e
promise.fail(e)
end
promise
end
def retweet_source(force_retrieve=false)
reblog
end
def retweet_source_d(force_retrieve=false)
promise = Delayer::Deferred.new(true)
Thread.new do
if reblog.is_a? Status
promise.call(reblog)
else
promise.fail(reblog)
end
rescue Exception => e
promise.fail(e)
end
promise
end
def retweet_ancestors(force_retrieve=false)
if reblog.is_a? Status
[self, reblog]
else
[self]
end
end
def rebloggable?(world = nil)
!actual_status.shared?(world) && !['private', 'direct'].include?(actual_status.visibility)
end
# 最終更新日時を取得する
def modified
@value[:modified] ||= [created, *(@retweets || []).map{ |x| x.modified }].compact.max
end
# 最終更新日時を更新する
def set_modified(time)
if modified < time
self[:modified] = time
Plugin::call(:message_modified, self)
end
self
end
def post(message:, **kwrest)
world, = Plugin.filtering(:world_current, nil)
Plugin[:mastodon].compose(self, world, body: message, **kwrest)
end
deprecate :post, :none, 2018, 11
def receive_user_idnames
mentions.map(&:acct)
end
end
end
|