]>
cat aescling's git repositories - mastodon.git/blob - app/lib/feed_manager.rb
1 # frozen_string_literal: true
10 # Must be <= MAX_ITEMS or the tracking sets will grow forever
13 def key ( type
, id
, subtype
= nil )
14 return "feed: #{type} : #{id} " unless subtype
16 "feed: #{type} : #{id} : #{subtype} "
19 def filter
?( timeline_type
, status
, receiver_id
)
20 if timeline_type
== :home
21 filter_from_home
?( status
, receiver_id
)
22 elsif timeline_type
== :mentions
23 filter_from_mentions
?( status
, receiver_id
)
29 def push_to_home ( account
, status
)
30 return false unless add_to_feed ( :home , account
. id
, status
)
31 trim ( :home , account
. id
)
32 PushUpdateWorker
. perform_async ( account
. id
, status
. id
, "timeline: #{account.id} " ) if push_update_required
?( "timeline: #{account.id} " )
36 def unpush_from_home ( account
, status
)
37 return false unless remove_from_feed ( :home , account
. id
, status
)
38 Redis
. current
. publish ( "timeline: #{account.id} " , Oj
. dump ( event
: :delete , payload
: status
. id
. to_s
))
42 def push_to_list ( list
, status
)
43 return false unless add_to_feed ( :list , list
. id
, status
)
45 PushUpdateWorker
. perform_async ( list
. account_id
, status
. id
, "timeline:list: #{list.id} " ) if push_update_required
?( "timeline:list: #{list.id} " )
49 def unpush_from_list ( list
, status
)
50 return false unless remove_from_feed ( :list , list
. id
, status
)
51 Redis
. current
. publish ( "timeline:list: #{list.id} " , Oj
. dump ( event
: :delete , payload
: status
. id
. to_s
))
55 def trim ( type
, account_id
)
56 timeline_key
= key ( type
, account_id
)
57 reblog_key
= key ( type
, account_id
, 'reblogs' )
59 # Remove any items past the MAX_ITEMS'th entry in our feed
60 redis
. zremrangebyrank ( timeline_key
, '0' , (-( FeedManager
::MAX_ITEMS +
1 )). to_s
)
62 # Get the score of the REBLOG_FALLOFF'th item in our feed, and stop
63 # tracking anything after it for deduplication purposes.
64 falloff_rank
= FeedManager
::REBLOG_FALLOFF - 1
65 falloff_range
= redis
. zrevrange ( timeline_key
, falloff_rank
, falloff_rank
, with_scores
: true )
66 falloff_score
= falloff_range
&. first
&. last
&. to_i
|| 0
68 # Get any reblogs we might have to clean up after.
69 redis
. zrangebyscore ( reblog_key
, 0 , falloff_score
). each
do | reblogged_id
|
70 # Remove it from the set of reblogs we're tracking *first* to avoid races.
71 redis
. zrem ( reblog_key
, reblogged_id
)
72 # Just drop any set we might have created to track additional reblogs.
73 # This means that if this reblog is deleted, we won't automatically insert
74 # another reblog, but also that any new reblog can be inserted into the
76 redis
. del ( key ( type
, account_id
, "reblogs: #{reblogged_id} " ))
80 def merge_into_timeline ( from_account
, into_account
)
81 timeline_key
= key ( :home , into_account
. id
)
82 query
= from_account
. statuses
. limit ( FeedManager
::MAX_ITEMS / 4 )
84 if redis
. zcard ( timeline_key
) >= FeedManager
::MAX_ITEMS / 4
85 oldest_home_score
= redis
. zrange ( timeline_key
, 0 , 0 , with_scores
: true )&. first
&. last
&. to_i
|| 0
86 query
= query
. where ( 'id > ?' , oldest_home_score
)
89 query
. each
do | status
|
90 next if status
. direct_visibility
? || filter
?( :home , status
, into_account
)
91 add_to_feed ( :home , into_account
. id
, status
)
94 trim ( :home , into_account
. id
)
97 def unmerge_from_timeline ( from_account
, into_account
)
98 timeline_key
= key ( :home , into_account
. id
)
99 oldest_home_score
= redis
. zrange ( timeline_key
, 0 , 0 , with_scores
: true )&. first
&. last
&. to_i
|| 0
101 from_account
. statuses
. select ( 'id, reblog_of_id' ). where ( 'id > ?' , oldest_home_score
). reorder ( nil ). find_each
do | status
|
102 remove_from_feed ( :home , into_account
. id
, status
)
106 def clear_from_timeline ( account
, target_account
)
107 timeline_key
= key ( :home , account
. id
)
108 timeline_status_ids
= redis
. zrange ( timeline_key
, 0 , - 1 )
109 target_statuses
= Status
. where ( id
: timeline_status_ids
, account
: target_account
)
111 target_statuses
. each
do | status
|
112 unpush_from_home ( account
, status
)
116 def populate_feed ( account
)
118 limit
= FeedManager
::MAX_ITEMS / 2
122 statuses
= Status
. as_home_timeline ( account
)
123 . paginate_by_max_id ( limit
, max_id
)
125 break if statuses
. empty
?
127 statuses
. each
do | status
|
128 next if filter_from_home
?( status
, account
)
129 added +
= 1 if add_to_feed ( :home , account
. id
, status
)
132 break unless added
. zero
?
134 max_id
= statuses
. last
. id
144 def push_update_required
?( timeline_id
)
145 redis
. exists ( "subscribed: #{timeline_id} " )
148 def blocks_or_mutes
?( receiver_id
, account_ids
, context
)
149 Block
. where ( account_id
: receiver_id
, target_account_id
: account_ids
). any
? ||
150 ( context
== :home ? Mute
. where ( account_id
: receiver_id
, target_account_id
: account_ids
). any
? : Mute
. where ( account_id
: receiver_id
, target_account_id
: account_ids
, hide_notifications
: true ). any
?)
153 def filter_from_home
?( status
, receiver_id
)
154 return false if receiver_id
== status
. account_id
155 return true if status
. reply
? && ( status
. in_reply_to_id
. nil ? || status
. in_reply_to_account_id
. nil ?)
156 return true if keyword_filter
?( status
, receiver_id
)
158 check_for_mutes
= [ status
. account_id
]
159 check_for_mutes
. concat ( status
. mentions
. pluck ( :account_id ))
160 check_for_mutes
. concat ([ status
. reblog
. account_id
]) if status
. reblog
?
162 return true if Mute
. where ( account_id
: receiver_id
, target_account_id
: check_for_mutes
). any
?
164 check_for_blocks
= status
. mentions
. pluck ( :account_id )
165 check_for_blocks
. concat ([ status
. account_id
])
166 check_for_blocks
. concat ([ status
. reblog
. account_id
]) if status
. reblog
?
168 return true if blocks_or_mutes
?( receiver_id
, check_for_blocks
, :home )
170 if status
. reply
? && !status
. in_reply_to_account_id
. nil ? # Filter out if it's a reply
171 should_filter
= !Follow
. where ( account_id
: receiver_id
, target_account_id
: status
. in_reply_to_account_id
). exists
? # and I'm not following the person it's a reply to
172 should_filter
&&= receiver_id !
= status
. in_reply_to_account_id
# and it's not a reply to me
173 should_filter
&&= status
. account_id !
= status
. in_reply_to_account_id
# and it's not a self-reply
175 elsif status
. reblog
? # Filter out a reblog
176 should_filter
= Follow
. where ( account_id
: receiver_id
, target_account_id
: status
. account_id
, show_reblogs
: false ). exists
? # if the reblogger's reblogs are suppressed
177 should_filter
||= Block
. where ( account_id
: status
. reblog
. account_id
, target_account_id
: receiver_id
). exists
? # or if the author of the reblogged status is blocking me
178 should_filter
||= AccountDomainBlock
. where ( account_id
: receiver_id
, domain
: status
. reblog
. account
. domain
). exists
? # or the author's domain is blocked
185 def keyword_filter
?( status
, receiver_id
)
186 Glitch
::KeywordMuteHelper . new ( receiver_id
). matches
?( status
)
189 def filter_from_mentions
?( status
, receiver_id
)
190 return true if receiver_id
== status
. account_id
192 # This filter is called from NotifyService, but already after the sender of
193 # the notification has been checked for mute/block. Therefore, it's not
194 # necessary to check the author of the toot for mute/block again
195 check_for_blocks
= status
. mentions
. pluck ( :account_id )
196 check_for_blocks
. concat ([ status
. in_reply_to_account
]) if status
. reply
? && !status
. in_reply_to_account_id
. nil ?
198 should_filter
= blocks_or_mutes
?( receiver_id
, check_for_blocks
, :mentions ) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted)
199 should_filter
||= ( status
. account
. silenced
? && !Follow
. where ( account_id
: receiver_id
, target_account_id
: status
. account_id
). exists
?) # of if the account is silenced and I'm not following them
200 should_filter
||= keyword_filter
?( status
, receiver_id
) # or if the mention contains a muted keyword
205 # Adds a status to an account's feed, returning true if a status was
206 # added, and false if it was not added to the feed. Note that this is
207 # an internal helper: callers must call trim or push updates if
208 # either action is appropriate.
209 def add_to_feed ( timeline_type
, account_id
, status
)
210 timeline_key
= key ( timeline_type
, account_id
)
211 reblog_key
= key ( timeline_type
, account_id
, 'reblogs' )
214 # If the original status or a reblog of it is within
215 # REBLOG_FALLOFF statuses from the top, do not re-insert it into
217 rank
= redis
. zrevrank ( timeline_key
, status
. reblog_of_id
)
219 return false if !rank
. nil ? && rank
< FeedManager
::REBLOG_FALLOFF
221 reblog_rank
= redis
. zrevrank ( reblog_key
, status
. reblog_of_id
)
224 # This is not something we've already seen reblogged, so we
225 # can just add it to the feed (and note that we're
227 redis
. zadd ( timeline_key
, status
. id
, status
. id
)
228 redis
. zadd ( reblog_key
, status
. id
, status
. reblog_of_id
)
230 # Another reblog of the same status was already in the
231 # REBLOG_FALLOFF most recent statuses, so we note that this
232 # is an "extra" reblog, by storing it in reblog_set_key.
233 reblog_set_key
= key ( timeline_type
, account_id
, "reblogs: #{status.reblog_of_id} " )
234 redis
. sadd ( reblog_set_key
, status
. id
)
238 # A reblog may reach earlier than the original status because of the
239 # delay of the worker deliverying the original status, the late addition
240 # by merging timelines, and other reasons.
241 # If such a reblog already exists, just do not re-insert it into the feed.
242 rank
= redis
. zrevrank ( reblog_key
, status
. id
)
244 return false unless rank
. nil ?
246 redis
. zadd ( timeline_key
, status
. id
, status
. id
)
252 # Removes an individual status from a feed, correctly handling cases
253 # with reblogs, and returning true if a status was removed. As with
254 # `add_to_feed`, this does not trigger push updates, so callers must
255 # do so if appropriate.
256 def remove_from_feed ( timeline_type
, account_id
, status
)
257 timeline_key
= key ( timeline_type
, account_id
)
260 # 1. If the reblogging status is not in the feed, stop.
261 status_rank
= redis
. zrevrank ( timeline_key
, status
. id
)
262 return false if status_rank
. nil ?
264 # 2. Remove reblog from set of this status's reblogs.
265 reblog_set_key
= key ( timeline_type
, account_id
, "reblogs: #{status.reblog_of_id} " )
267 redis
. srem ( reblog_set_key
, status
. id
)
268 # 3. Re-insert another reblog or original into the feed if one
269 # remains in the set. We could pick a random element, but this
270 # set should generally be small, and it seems ideal to show the
271 # oldest potential such reblog.
272 other_reblog
= redis
. smembers ( reblog_set_key
). map (& :to_i ). sort
. first
274 redis
. zadd ( timeline_key
, other_reblog
, other_reblog
) if other_reblog
276 # 4. Remove the reblogging status from the feed (as normal)
277 # (outside conditional)
279 # If the original is getting deleted, no use for reblog references
280 redis
. del ( key ( timeline_type
, account_id
, "reblogs: #{status.id} " ))
283 redis
. zrem ( timeline_key
, status
. id
)
This page took 0.266076 seconds and 4 git commands to generate.