about summary refs log tree commit diff
path: root/app/services/fan_out_on_write_service.rb
blob: 3bd3717fe35ea5057b6f890d06ecd22a8ddd3207 (plain) (blame)
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
# frozen_string_literal: true

class FanOutOnWriteService < BaseService
  # Push a status into home and mentions feeds
  # @param [Status] status
  def call(status, delayed: false, public_only: false)
    raise Mastodon::RaceConditionError if status.visibility.nil?

    deliver_to_self(status) if status.account.local?
    return if delayed || status.hidden?

    render_anonymous_payload(status)

    if status.direct_visibility?
      return if public_only || status.curated
      deliver_to_mentioned_followers(status)
      deliver_to_direct_timelines(status)
      deliver_to_own_conversation(status)
    elsif status.limited_visibility?
      return if public_only || status.curated
      deliver_to_mentioned_followers(status)
    elsif status.local_visibility?
      deliver_to_followers(status) unless public_only || status.curated
      return if status.reblog?
      deliver_to_lists(status) unless public_only || status.curated
      deliver_to_local(status) unless filtered?(status)
    else
      unless public_only || status.curated
        deliver_to_followers(status)
        deliver_to_lists(status)
      end

      return if status.reblog?
      return if filtered?(status) || (status.reblog? && filtered?(status.reblog))

      if !status.reblog? && status.distributable?
        deliver_to_hashtags(status)
        deliver_to_public(status) if status.curated
      end

      if status.relayed?
        status = Status.find(status.reblog_of_id)
        return if filtered?(status)
        render_anonymous_payload(status)
      end

      return unless status.network? && status.public_visibility? && !status.reblog

      deliver_to_local(status)
    end
  end

  private

  def filtered?(status)
    status.account.silenced? || status.reply? && status.in_reply_to_account_id != status.account_id
  end

  def deliver_to_self(status)
    Rails.logger.debug "Delivering status #{status.id} to author"
    FeedManager.instance.push_to_home(status.account, status)
  end

  def deliver_to_followers(status)
    Rails.logger.debug "Delivering status #{status.id} to followers"

    status.account.followers_for_local_distribution.select(:id).reorder(nil).find_in_batches do |followers|
      FeedInsertWorker.push_bulk(followers) do |follower|
        [status.id, follower.id, :home]
      end
    end
  end

  def deliver_to_lists(status)
    Rails.logger.debug "Delivering status #{status.id} to lists"

    List.where(account_id: status.account.id, show_self: true).select(:id).reorder(nil).find_in_batches do |lists|
      FeedInsertWorker.push_bulk(lists) do |list|
        [status.id, list.id, :list]
      end
    end

    status.account.lists_for_local_distribution.select(:id).reorder(nil).find_in_batches do |lists|
      FeedInsertWorker.push_bulk(lists) do |list|
        [status.id, list.id, :list]
      end
    end
  end

  def deliver_to_mentioned_followers(status)
    Rails.logger.debug "Delivering status #{status.id} to limited followers"

    FeedInsertWorker.push_bulk(status.mentions.includes(:account).map(&:account).select { |mentioned_account| mentioned_account.local? && mentioned_account.following?(status.account) }) do |follower|
      [status.id, follower.id, :home]
    end
  end

  def render_anonymous_payload(status)
    @payload = InlineRenderer.render(status, nil, :status)
    @payload = Oj.dump(event: :update, payload: @payload)
  end

  def deliver_to_hashtags(status)
    Rails.logger.debug "Delivering status #{status.id} to hashtags"

    status.tags.reject { |t| t.private }.pluck(:name).each do |hashtag|
      Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
      Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if status.local?
    end
  end

  def deliver_to_public(status)
    Rails.logger.debug "Delivering status #{status.id} to public timeline"

    Redis.current.publish('timeline:public', @payload)
    Redis.current.publish('timeline:public:media', @payload) if status.media_attachments.any?
  end

  def deliver_to_local(status)
    Rails.logger.debug "Delivering status #{status.id} to local timeline"

    Redis.current.publish('timeline:public:local', @payload)
    Redis.current.publish('timeline:public:local:media', @payload) if status.media_attachments.any?
  end

  def deliver_to_direct_timelines(status)
    Rails.logger.debug "Delivering status #{status.id} to direct timelines"

    status.mentions.includes(:account).each do |mention|
      Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local?
    end

    Redis.current.publish("timeline:direct:#{status.account.id}", @payload) if status.account.local?
  end

  def deliver_to_own_conversation(status)
    AccountConversation.add_status(status.account, status)
  end
end