Fix subscriptions:clear task, refactor feeds, refactor streamable activites
and atom feed generation to some extent, as well as the way mentions are stored
This commit is contained in:
		
							parent
							
								
									9594f0e858
								
							
						
					
					
						commit
						a08e724476
					
				|  | @ -11,8 +11,8 @@ class AccountsController < ApplicationController | ||||||
|       format.atom do |       format.atom do | ||||||
|         @entries = @account.stream_entries.order('id desc').with_includes.paginate_by_max_id(20, params[:max_id] || nil) |         @entries = @account.stream_entries.order('id desc').with_includes.paginate_by_max_id(20, params[:max_id] || nil) | ||||||
| 
 | 
 | ||||||
|         ActiveRecord::Associations::Preloader.new.preload(@entries.select { |a| a.activity_type == 'Status' }, activity: [:mentioned_accounts, reblog: :account, thread: :account]) |         ActiveRecord::Associations::Preloader.new.preload(@entries.select { |a| a.activity_type == 'Status' }, activity: [:mentions, reblog: :account, thread: :account]) | ||||||
|         ActiveRecord::Associations::Preloader.new.preload(@entries.select { |a| a.activity_type == 'Favourite' }, activity: [:account, :thread, :mentioned_accounts]) |         ActiveRecord::Associations::Preloader.new.preload(@entries.select { |a| a.activity_type == 'Favourite' }, activity: [:account, :status]) | ||||||
|         ActiveRecord::Associations::Preloader.new.preload(@entries.select { |a| a.activity_type == 'Follow' }, activity: :target_account) |         ActiveRecord::Associations::Preloader.new.preload(@entries.select { |a| a.activity_type == 'Follow' }, activity: :target_account) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -22,12 +22,10 @@ class Api::StatusesController < ApiController | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def home |   def home | ||||||
|     feed      = Feed.new(:home, current_user.account) |     @statuses = Feed.new(:home, current_user.account).get(20, params[:max_id]) | ||||||
|     @statuses = feed.get(20, params[:max_id] || '+inf') |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def mentions |   def mentions | ||||||
|     feed      = Feed.new(:mentions, current_user.account) |     @statuses = Feed.new(:mentions, current_user.account).get(20, params[:max_id]) | ||||||
|     @statuses = feed.get(20, params[:max_id] || '+inf') |  | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ class StatusesController < ApplicationController | ||||||
|   before_action :authenticate_user! |   before_action :authenticate_user! | ||||||
| 
 | 
 | ||||||
|   def create |   def create | ||||||
|     status = PostStatusService.new.(current_user.account, status_params[:text]) |     PostStatusService.new.(current_user.account, status_params[:text]) | ||||||
|     redirect_to root_path |     redirect_to root_path | ||||||
|   rescue ActiveRecord::RecordInvalid |   rescue ActiveRecord::RecordInvalid | ||||||
|     redirect_to root_path |     redirect_to root_path | ||||||
|  |  | ||||||
|  | @ -20,13 +20,25 @@ module ApplicationHelper | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def linkify(status) |   def account_from_mentions(search_string, mentions) | ||||||
|     mention_hash = {} |     mentions.each { |x| return x.account if x.account.acct.eql?(search_string) } | ||||||
|     status.mentions.each { |m| mention_hash[m.acct] = m } |  | ||||||
|     coder = HTMLEntities.new |  | ||||||
| 
 | 
 | ||||||
|     auto_link(coder.encode(status.text), link: :urls, html: { rel: 'nofollow noopener' }).gsub(Account::MENTION_RE) do |m| |     # If that was unsuccessful, try fetching user from db separately | ||||||
|       account = mention_hash[Account::MENTION_RE.match(m)[1]] |     # But this shouldn't ever happen if the mentions were created correctly! | ||||||
|  |     username, domain = search_string.split('@') | ||||||
|  | 
 | ||||||
|  |     if domain == Rails.configuration.x.local_domain | ||||||
|  |       account = Account.find_local(username) | ||||||
|  |     else | ||||||
|  |       account = Account.find_by(username: username, domain: domain) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     account | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def linkify(status) | ||||||
|  |     auto_link(HTMLEntities.new.encode(status.text), link: :urls, html: { rel: 'nofollow noopener' }).gsub(Account::MENTION_RE) do |m| | ||||||
|  |       account = account_from_mentions(Account::MENTION_RE.match(m)[1], status.mentions) | ||||||
|       "#{m.split('@').first}<a href=\"#{url_for_target(account)}\" class=\"mention\">@<span>#{account.acct}</span></a>" |       "#{m.split('@').first}<a href=\"#{url_for_target(account)}\" class=\"mention\">@<span>#{account.acct}</span></a>" | ||||||
|     end.html_safe |     end.html_safe | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -135,6 +135,10 @@ module AtomBuilderHelper | ||||||
|     xml.logo url |     xml.logo url | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def email(xml, email) | ||||||
|  |     xml.email email | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def conditionally_formatted(activity) |   def conditionally_formatted(activity) | ||||||
|     if activity.is_a?(Status) |     if activity.is_a?(Status) | ||||||
|       content_for_status(activity.reblog? ? activity.reblog : activity) |       content_for_status(activity.reblog? ? activity.reblog : activity) | ||||||
|  | @ -149,6 +153,7 @@ module AtomBuilderHelper | ||||||
|     object_type      xml, :person |     object_type      xml, :person | ||||||
|     uri              xml, url_for_target(account) |     uri              xml, url_for_target(account) | ||||||
|     name             xml, account.username |     name             xml, account.username | ||||||
|  |     email            xml, account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct | ||||||
|     summary          xml, account.note |     summary          xml, account.note | ||||||
|     link_alternate   xml, url_for_target(account) |     link_alternate   xml, url_for_target(account) | ||||||
|     link_avatar      xml, account |     link_avatar      xml, account | ||||||
|  | @ -171,16 +176,13 @@ module AtomBuilderHelper | ||||||
| 
 | 
 | ||||||
|     if stream_entry.targeted? |     if stream_entry.targeted? | ||||||
|       target(xml) do |       target(xml) do | ||||||
|  |         if stream_entry.target.object_type == :person | ||||||
|  |           include_author xml, stream_entry.target | ||||||
|  |         else | ||||||
|           object_type    xml, stream_entry.target.object_type |           object_type    xml, stream_entry.target.object_type | ||||||
|           simple_id      xml, uri_for_target(stream_entry.target) |           simple_id      xml, uri_for_target(stream_entry.target) | ||||||
|           title          xml, stream_entry.target.title |           title          xml, stream_entry.target.title | ||||||
|           link_alternate xml, url_for_target(stream_entry.target) |           link_alternate xml, url_for_target(stream_entry.target) | ||||||
| 
 |  | ||||||
|         # People have summary and portable contacts information |  | ||||||
|         if stream_entry.target.object_type == :person |  | ||||||
|           summary          xml, stream_entry.target.content |  | ||||||
|           portable_contact xml, stream_entry.target |  | ||||||
|           link_avatar      xml, stream_entry.target |  | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         # Statuses have content and author |         # Statuses have content and author | ||||||
|  |  | ||||||
|  | @ -0,0 +1,11 @@ | ||||||
|  | class FeedManager | ||||||
|  |   MAX_ITEMS = 800 | ||||||
|  | 
 | ||||||
|  |   def self.key(type, id) | ||||||
|  |     "feed:#{type}:#{id}" | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def self.filter_status?(status, follower) | ||||||
|  |     (status.reply? && !(follower.id = replied_to_user.id || follower.following?(replied_to_user))) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -1,4 +1,6 @@ | ||||||
| class Account < ActiveRecord::Base | class Account < ActiveRecord::Base | ||||||
|  |   include Targetable | ||||||
|  | 
 | ||||||
|   # Local users |   # Local users | ||||||
|   has_one :user, inverse_of: :account |   has_one :user, inverse_of: :account | ||||||
|   validates :username, uniqueness: { scope: :domain, case_sensitive: false }, if:     'local?' |   validates :username, uniqueness: { scope: :domain, case_sensitive: false }, if:     'local?' | ||||||
|  | @ -52,18 +54,6 @@ class Account < ActiveRecord::Base | ||||||
|     local? ? self.username : "#{self.username}@#{self.domain}" |     local? ? self.username : "#{self.username}@#{self.domain}" | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def object_type |  | ||||||
|     :person |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def title |  | ||||||
|     self.username |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def content |  | ||||||
|     self.note |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def subscribed? |   def subscribed? | ||||||
|     !(self.secret.blank? || self.verify_token.blank?) |     !(self.secret.blank? || self.verify_token.blank?) | ||||||
|   end |   end | ||||||
|  | @ -97,6 +87,10 @@ class Account < ActiveRecord::Base | ||||||
|     self[:avatar_remote_url] = url |     self[:avatar_remote_url] = url | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def object_type | ||||||
|  |     :person | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def to_param |   def to_param | ||||||
|     self.username |     self.username | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,31 @@ | ||||||
|  | module Streamable | ||||||
|  |   extend ActiveSupport::Concern | ||||||
|  | 
 | ||||||
|  |   included do | ||||||
|  |     has_one :stream_entry, as: :activity | ||||||
|  | 
 | ||||||
|  |     def title | ||||||
|  |       super | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def content | ||||||
|  |       title | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def target | ||||||
|  |       super | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def object_type | ||||||
|  |       :activity | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def thread | ||||||
|  |       super | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     after_create do | ||||||
|  |       self.account.stream_entries.create!(activity: self) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | module Targetable | ||||||
|  |   extend ActiveSupport::Concern | ||||||
|  | 
 | ||||||
|  |   included do | ||||||
|  |     def object_type | ||||||
|  |       :object | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| class Favourite < ActiveRecord::Base | class Favourite < ActiveRecord::Base | ||||||
|  |   include Streamable | ||||||
|  | 
 | ||||||
|   belongs_to :account, inverse_of: :favourites |   belongs_to :account, inverse_of: :favourites | ||||||
|   belongs_to :status,  inverse_of: :favourites |   belongs_to :status,  inverse_of: :favourites | ||||||
| 
 | 
 | ||||||
|   has_one :stream_entry, as: :activity |  | ||||||
| 
 |  | ||||||
|   def verb |   def verb | ||||||
|     :favorite |     :favorite | ||||||
|   end |   end | ||||||
|  | @ -12,27 +12,15 @@ class Favourite < ActiveRecord::Base | ||||||
|     "#{self.account.acct} favourited a status by #{self.status.account.acct}" |     "#{self.account.acct} favourited a status by #{self.status.account.acct}" | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def content |  | ||||||
|     title |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def object_type |   def object_type | ||||||
|     target.object_type |     target.object_type | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def target |   def thread | ||||||
|     self.status |     self.status | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def mentions |   def target | ||||||
|     [] |     thread | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def thread |  | ||||||
|     target |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   after_create do |  | ||||||
|     self.account.stream_entries.create!(activity: self) |  | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -4,21 +4,24 @@ class Feed | ||||||
|     @account = account |     @account = account | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def get(limit, max_id = '+inf') |   def get(limit, max_id) | ||||||
|  |     max_id     = '+inf' if max_id.nil? | ||||||
|     unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", '-inf', limit: [0, limit]) |     unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", '-inf', limit: [0, limit]) | ||||||
|     status_map = Hash.new |     status_map = Hash.new | ||||||
| 
 | 
 | ||||||
|     # If we're after most recent items and none are there, we need to precompute the feed |     # If we're after most recent items and none are there, we need to precompute the feed | ||||||
|     return PrecomputeFeedService.new.(@type, @account).take(limit) if unhydrated.empty? && max_id == '+inf' |     if unhydrated.empty? && max_id == '+inf' | ||||||
| 
 |       PrecomputeFeedService.new.(@type, @account, limit) | ||||||
|  |     else | ||||||
|       Status.where(id: unhydrated).with_includes.with_counters.each { |status| status_map[status.id.to_s] = status } |       Status.where(id: unhydrated).with_includes.with_counters.each { |status| status_map[status.id.to_s] = status } | ||||||
|     return unhydrated.map { |id| status_map[id] }.compact |       unhydrated.map { |id| status_map[id] }.compact | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def key |   def key | ||||||
|     "feed:#{@type}:#{@account.id}" |     FeedManager.key(@type, @account.id) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def redis |   def redis | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| class Follow < ActiveRecord::Base | class Follow < ActiveRecord::Base | ||||||
|  |   include Streamable | ||||||
|  | 
 | ||||||
|   belongs_to :account |   belongs_to :account | ||||||
|   belongs_to :target_account, class_name: 'Account' |   belongs_to :target_account, class_name: 'Account' | ||||||
| 
 | 
 | ||||||
|   has_one :stream_entry, as: :activity |  | ||||||
| 
 |  | ||||||
|   validates :account, :target_account, presence: true |   validates :account, :target_account, presence: true | ||||||
|   validates :account_id, uniqueness: { scope: :target_account_id } |   validates :account_id, uniqueness: { scope: :target_account_id } | ||||||
| 
 | 
 | ||||||
|  | @ -16,22 +16,10 @@ class Follow < ActiveRecord::Base | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def object_type |   def object_type | ||||||
|     target.object_type |     :person | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def content |  | ||||||
|     self.destroyed? ? "#{self.account.acct} is no longer following #{self.target_account.acct}" : "#{self.account.acct} started following #{self.target_account.acct}" |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def title |   def title | ||||||
|     content |     self.destroyed? ? "#{self.account.acct} is no longer following #{self.target_account.acct}" : "#{self.account.acct} started following #{self.target_account.acct}" | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def mentions |  | ||||||
|     [] |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   after_create do |  | ||||||
|     self.account.stream_entries.create!(activity: self) |  | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -1,24 +1,23 @@ | ||||||
| class Status < ActiveRecord::Base | class Status < ActiveRecord::Base | ||||||
|   include Paginable |   include Paginable | ||||||
|  |   include Streamable | ||||||
| 
 | 
 | ||||||
|   belongs_to :account, inverse_of: :statuses |   belongs_to :account, inverse_of: :statuses | ||||||
| 
 | 
 | ||||||
|   belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies |   belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies | ||||||
|   belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs |   belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs | ||||||
| 
 | 
 | ||||||
|   has_one :stream_entry, as: :activity |  | ||||||
| 
 |  | ||||||
|   has_many :favourites, inverse_of: :status, dependent: :destroy |   has_many :favourites, inverse_of: :status, dependent: :destroy | ||||||
|   has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy |   has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy | ||||||
|   has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread |   has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread | ||||||
|   has_many :mentioned_accounts, class_name: 'Mention', dependent: :destroy |   has_many :mentions, dependent: :destroy | ||||||
| 
 | 
 | ||||||
|   validates :account, presence: true |   validates :account, presence: true | ||||||
|   validates :uri, uniqueness: true, unless: 'local?' |   validates :uri, uniqueness: true, unless: 'local?' | ||||||
|   validates :text, presence: true, if: Proc.new { |s| s.local? && !s.reblog? } |   validates :text, presence: true, if: Proc.new { |s| s.local? && !s.reblog? } | ||||||
| 
 | 
 | ||||||
|   scope :with_counters, -> { select('statuses.*, (select count(r.id) from statuses as r where r.reblog_of_id = statuses.id) as reblogs_count, (select count(f.id) from favourites as f where f.status_id = statuses.id) as favourites_count') } |   scope :with_counters, -> { select('statuses.*, (select count(r.id) from statuses as r where r.reblog_of_id = statuses.id) as reblogs_count, (select count(f.id) from favourites as f where f.status_id = statuses.id) as favourites_count') } | ||||||
|   scope :with_includes, -> { includes(:account, :mentioned_accounts, reblog: [:account, :mentioned_accounts], thread: [:account, :mentioned_accounts]) } |   scope :with_includes, -> { includes(:account, :mentions, reblog: [:account, :mentions], thread: [:account, :mentions]) } | ||||||
| 
 | 
 | ||||||
|   def local? |   def local? | ||||||
|     self.uri.nil? |     self.uri.nil? | ||||||
|  | @ -60,18 +59,6 @@ class Status < ActiveRecord::Base | ||||||
|     self.attributes['favourites_count'] || self.favourites.count |     self.attributes['favourites_count'] || self.favourites.count | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def mentions |  | ||||||
|     if @mentions.nil? |  | ||||||
|       @mentions = [] |  | ||||||
|       @mentions << thread.account if reply? |  | ||||||
|       @mentions << reblog.account if reblog? |  | ||||||
|       self.mentioned_accounts.each { |mention| @mentions << mention.account } unless reblog? |  | ||||||
|       @mentions = @mentions.uniq |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     @mentions |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def ancestors |   def ancestors | ||||||
|     Status.where(id: Status.find_by_sql(['WITH RECURSIVE search_tree(id, in_reply_to_id, path) AS (SELECT id, in_reply_to_id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, statuses.in_reply_to_id, path || statuses.id FROM search_tree JOIN statuses ON statuses.id = search_tree.in_reply_to_id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path DESC', self.id]) - [self]) |     Status.where(id: Status.find_by_sql(['WITH RECURSIVE search_tree(id, in_reply_to_id, path) AS (SELECT id, in_reply_to_id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, statuses.in_reply_to_id, path || statuses.id FROM search_tree JOIN statuses ON statuses.id = search_tree.in_reply_to_id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path DESC', self.id]) - [self]) | ||||||
|   end |   end | ||||||
|  | @ -80,7 +67,11 @@ class Status < ActiveRecord::Base | ||||||
|     Status.where(id: Status.find_by_sql(['WITH RECURSIVE search_tree(id, path) AS (SELECT id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, path || statuses.id FROM search_tree JOIN statuses ON statuses.in_reply_to_id = search_tree.id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path', self.id]) - [self]) |     Status.where(id: Status.find_by_sql(['WITH RECURSIVE search_tree(id, path) AS (SELECT id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, path || statuses.id FROM search_tree JOIN statuses ON statuses.in_reply_to_id = search_tree.id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path', self.id]) - [self]) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   after_create do |   def self.as_home_timeline(account) | ||||||
|     self.account.stream_entries.create!(activity: self) |     self.where(account: [account] + account.following).with_includes.with_counters | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def self.as_mentions_timeline(account) | ||||||
|  |     self.where(id: Mention.where(account: account).pluck(:status_id)).with_includes.with_counters | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -41,7 +41,7 @@ class StreamEntry < ActiveRecord::Base | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def mentions |   def mentions | ||||||
|     orphaned? ? [] : self.activity.mentions |     self.activity.respond_to?(:mentions) ? self.activity.mentions.map { |x| x.account } : [] | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
|  |  | ||||||
|  | @ -1,6 +1,4 @@ | ||||||
| class FanOutOnWriteService < BaseService | class FanOutOnWriteService < BaseService | ||||||
|   MAX_FEED_SIZE = 800 |  | ||||||
| 
 |  | ||||||
|   # Push a status into home and mentions feeds |   # Push a status into home and mentions feeds | ||||||
|   # @param [Status] status |   # @param [Status] status | ||||||
|   def call(status) |   def call(status) | ||||||
|  | @ -17,13 +15,13 @@ class FanOutOnWriteService < BaseService | ||||||
| 
 | 
 | ||||||
|   def deliver_to_followers(status, replied_to_user) |   def deliver_to_followers(status, replied_to_user) | ||||||
|     status.account.followers.each do |follower| |     status.account.followers.each do |follower| | ||||||
|       next if (status.reply? && !(follower.id = replied_to_user.id || follower.following?(replied_to_user))) || !follower.local? |       next if !follower.local? || FeedManager.filter_status?(status, follower) | ||||||
|       push(:home, follower.id, status) |       push(:home, follower.id, status) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def deliver_to_mentioned(status) |   def deliver_to_mentioned(status) | ||||||
|     status.mentioned_accounts.each do |mention| |     status.mentions.each do |mention| | ||||||
|       mentioned_account = mention.account |       mentioned_account = mention.account | ||||||
|       next unless mentioned_account.local? |       next unless mentioned_account.local? | ||||||
|       push(:mentions, mentioned_account.id, status) |       push(:mentions, mentioned_account.id, status) | ||||||
|  | @ -31,19 +29,15 @@ class FanOutOnWriteService < BaseService | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def push(type, receiver_id, status) |   def push(type, receiver_id, status) | ||||||
|     redis.zadd(key(type, receiver_id), status.id, status.id) |     redis.zadd(FeedManager.key(type, receiver_id), status.id, status.id) | ||||||
|     trim(type, receiver_id) |     trim(type, receiver_id) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def trim(type, receiver_id) |   def trim(type, receiver_id) | ||||||
|     return unless redis.zcard(key(type, receiver_id)) > MAX_FEED_SIZE |     return unless redis.zcard(FeedManager.key(type, receiver_id)) > FeedManager::MAX_ITEMS | ||||||
| 
 | 
 | ||||||
|     last = redis.zrevrange(key(type, receiver_id), MAX_FEED_SIZE - 1, MAX_FEED_SIZE - 1) |     last = redis.zrevrange(FeedManager.key(type, receiver_id), FeedManager::MAX_ITEMS - 1, FeedManager::MAX_ITEMS - 1) | ||||||
|     redis.zremrangebyscore(key(type, receiver_id), '-inf', "(#{last.last}") |     redis.zremrangebyscore(FeedManager.key(type, receiver_id), '-inf', "(#{last.last}") | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def key(type, id) |  | ||||||
|     "feed:#{type}:#{id}" |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def redis |   def redis | ||||||
|  |  | ||||||
|  | @ -1,34 +1,22 @@ | ||||||
| class PrecomputeFeedService < BaseService | class PrecomputeFeedService < BaseService | ||||||
|   MAX_FEED_SIZE = 800 |   # Fill up a user's home/mentions feed from DB and return a subset | ||||||
| 
 |  | ||||||
|   # Fill up a user's home/mentions feed from DB and return it |  | ||||||
|   # @param [Symbol] type :home or :mentions |   # @param [Symbol] type :home or :mentions | ||||||
|   # @param [Account] account |   # @param [Account] account | ||||||
|   # @return [Array] |   # @return [Array] | ||||||
|   def call(type, account) |   def call(type, account, limit) | ||||||
|     statuses = send(type.to_s, account).order('created_at desc').limit(MAX_FEED_SIZE) |     instant_return = [] | ||||||
|     statuses.each { |status| push(type, account.id, status) } | 
 | ||||||
|     statuses |     Status.send("as_#{type}_timeline", account).order('created_at desc').limit(FeedManager::MAX_ITEMS).each do |status| | ||||||
|  |       next if type == :home && FeedManager.filter_status?(status, account) | ||||||
|  |       redis.zadd(FeedManager.key(type, receiver_id), status.id, status.id) | ||||||
|  |       instant_return << status unless instant_return.size > limit | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     instant_return | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def push(type, receiver_id, status) |  | ||||||
|     redis.zadd(key(type, receiver_id), status.id, status.id) |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def home(account) |  | ||||||
|     Status.where(account: [account] + account.following).with_includes.with_counters |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def mentions(account) |  | ||||||
|     Status.where(id: Mention.where(account: account).pluck(:status_id)).with_includes.with_counters |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def key(type, id) |  | ||||||
|     "feed:#{type}:#{id}" |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def redis |   def redis | ||||||
|     $redis |     $redis | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -4,25 +4,24 @@ class ProcessFeedService < BaseService | ||||||
|   # @param [Account] account Account this feed belongs to |   # @param [Account] account Account this feed belongs to | ||||||
|   def call(body, account) |   def call(body, account) | ||||||
|     xml = Nokogiri::XML(body) |     xml = Nokogiri::XML(body) | ||||||
| 
 |     update_remote_profile_service.(xml.at_xpath('/xmlns:feed/xmlns:author'), account) unless xml.at_xpath('/xmlns:feed').nil? | ||||||
|     # If we got a full feed, make sure the account's profile is up to date |     xml.xpath('//xmlns:entry').each { |entry| process_entry(account, entry) } | ||||||
|     unless xml.at_xpath('/xmlns:feed').nil? |  | ||||||
|       update_remote_profile_service.(xml.at_xpath('/xmlns:feed/xmlns:author'), account) |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|     # Process entries |   private | ||||||
|     xml.xpath('//xmlns:entry').each do |entry| | 
 | ||||||
|       next unless [:note, :comment, :activity].include? object_type(entry) |   def process_entry(account, entry) | ||||||
|  |     return unless [:note, :comment, :activity].include? object_type(entry) | ||||||
| 
 | 
 | ||||||
|     status = Status.find_by(uri: activity_id(entry)) |     status = Status.find_by(uri: activity_id(entry)) | ||||||
| 
 | 
 | ||||||
|     # If we already have a post and the verb is now "delete", we gotta delete it and move on! |     # If we already have a post and the verb is now "delete", we gotta delete it and move on! | ||||||
|     if !status.nil? && verb(entry) == :delete |     if !status.nil? && verb(entry) == :delete | ||||||
|       delete_post!(status) |       delete_post!(status) | ||||||
|         next |       return | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|       next unless status.nil? |     return unless status.nil? | ||||||
| 
 | 
 | ||||||
|     status = Status.new(uri: activity_id(entry), url: activity_link(entry), account: account, text: content(entry), created_at: published(entry), updated_at: updated(entry)) |     status = Status.new(uri: activity_id(entry), url: activity_link(entry), account: account, text: content(entry), created_at: published(entry), updated_at: updated(entry)) | ||||||
| 
 | 
 | ||||||
|  | @ -38,31 +37,34 @@ class ProcessFeedService < BaseService | ||||||
| 
 | 
 | ||||||
|     # If we added a status, go through accounts it mentions and create respective relations |     # If we added a status, go through accounts it mentions and create respective relations | ||||||
|     unless status.new_record? |     unless status.new_record? | ||||||
|         entry.xpath('./xmlns:link[@rel="mentioned"]').each do |mention_link| |       record_remote_mentions(status, entry.xpath('./xmlns:link[@rel="mentioned"]')) | ||||||
|  |       fan_out_on_write_service.(status) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def record_remote_mentions(status, links) | ||||||
|     # Here we have to do a reverse lookup of local accounts by their URL! |     # Here we have to do a reverse lookup of local accounts by their URL! | ||||||
|     # It's not pretty at all! I really wish all these protocols sticked to |     # It's not pretty at all! I really wish all these protocols sticked to | ||||||
|     # using acct:username@domain only! It would make things so much easier |     # using acct:username@domain only! It would make things so much easier | ||||||
|     # and tidier |     # and tidier | ||||||
| 
 | 
 | ||||||
|  |     links.each do |mention_link| | ||||||
|       href = Addressable::URI.parse(mention_link.attribute('href').value) |       href = Addressable::URI.parse(mention_link.attribute('href').value) | ||||||
| 
 | 
 | ||||||
|       if href.host == Rails.configuration.x.local_domain |       if href.host == Rails.configuration.x.local_domain | ||||||
|  |         # A local user is mentioned | ||||||
|         mentioned_account = Account.find_local(href.path.gsub('/users/', '')) |         mentioned_account = Account.find_local(href.path.gsub('/users/', '')) | ||||||
| 
 | 
 | ||||||
|         unless mentioned_account.nil? |         unless mentioned_account.nil? | ||||||
|           mentioned_account.mentions.where(status: status).first_or_create(status: status) |           mentioned_account.mentions.where(status: status).first_or_create(status: status) | ||||||
|           NotificationMailer.mention(mentioned_account, status).deliver_later |           NotificationMailer.mention(mentioned_account, status).deliver_later | ||||||
|         end |         end | ||||||
|           end |       else | ||||||
|         end |         # What to do about remote user? | ||||||
| 
 |  | ||||||
|         fan_out_on_write_service.(status) |  | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |  | ||||||
| 
 |  | ||||||
|   def add_post!(_entry, status) |   def add_post!(_entry, status) | ||||||
|     status.save! |     status.save! | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ class ProcessMentionsService < BaseService | ||||||
|       mentioned_account.mentions.where(status: status).first_or_create(status: status) |       mentioned_account.mentions.where(status: status).first_or_create(status: status) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     status.mentioned_accounts.each do |mention| |     status.mentions.each do |mention| | ||||||
|       mentioned_account = mention.account |       mentioned_account = mention.account | ||||||
| 
 | 
 | ||||||
|       if mentioned_account.local? |       if mentioned_account.local? | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ namespace :subscriptions do | ||||||
|     accounts = Account.where('(select count(f.id) from follows as f where f.target_account_id = accounts.id) = 0').where.not(domain: nil) |     accounts = Account.where('(select count(f.id) from follows as f where f.target_account_id = accounts.id) = 0').where.not(domain: nil) | ||||||
| 
 | 
 | ||||||
|     accounts.each do |a| |     accounts.each do |a| | ||||||
|       a.subscription(api_subscription_url(a.id)).unsubscribe |       a.subscription('').unsubscribe | ||||||
|       a.update!(verify_token: '', secret: '') |       a.update!(verify_token: '', secret: '') | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -30,6 +30,29 @@ RSpec.describe ApplicationHelper, type: :helper do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#linkify' do |   describe '#linkify' do | ||||||
|     pending |     let(:alice) { Fabricate(:account, username: 'alice') } | ||||||
|  |     let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', url: 'http://example.com/bob') } | ||||||
|  | 
 | ||||||
|  |     it 'turns mention of remote user into link' do | ||||||
|  |       status = Fabricate(:status, text: 'Hello @bob@example.com', account: bob) | ||||||
|  |       status.mentions.create(account: bob) | ||||||
|  |       expect(helper.linkify(status)).to match('<a href="http://example.com/bob" class="mention">@<span>bob@example.com</span></a>') | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'turns mention of local user into link' do | ||||||
|  |       status = Fabricate(:status, text: 'Hello @alice', account: bob) | ||||||
|  |       status.mentions.create(account: alice) | ||||||
|  |       expect(helper.linkify(status)).to match('<a href="http://test.host/users/alice" class="mention">@<span>alice</span></a>') | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#account_from_mentions' do | ||||||
|  |     let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') } | ||||||
|  |     let(:status) { Fabricate(:status, text: 'Hello @bob@example.com', account: bob) } | ||||||
|  |     let(:mentions) { [Mention.create(status: status, account: bob)] } | ||||||
|  | 
 | ||||||
|  |     it 'returns account' do | ||||||
|  |       expect(helper.account_from_mentions('bob@example.com', mentions)).to eq bob | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -96,18 +96,6 @@ RSpec.describe Account, type: :model do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#title' do |  | ||||||
|     it 'is the same as the username' do |  | ||||||
|       expect(subject.title).to eql subject.username |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   describe '#content' do |  | ||||||
|     it 'is the same as the note' do |  | ||||||
|       expect(subject.content).to eql subject.note |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   describe '#ping!' do |   describe '#ping!' do | ||||||
|     pending |     pending | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -42,12 +42,6 @@ RSpec.describe Favourite, type: :model do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#mentions' do |  | ||||||
|     it 'is always empty' do |  | ||||||
|       expect(subject.mentions).to be_empty |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   describe '#thread' do |   describe '#thread' do | ||||||
|     it 'equals the target' do |     it 'equals the target' do | ||||||
|       expect(subject.thread).to eq subject.target |       expect(subject.thread).to eq subject.target | ||||||
|  |  | ||||||
|  | @ -35,10 +35,4 @@ RSpec.describe Follow, type: :model do | ||||||
|       expect(subject.target).to eq bob |       expect(subject.target).to eq bob | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 |  | ||||||
|   describe '#mentions' do |  | ||||||
|     it 'is empty' do |  | ||||||
|       expect(subject.mentions).to be_empty |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -40,31 +40,6 @@ RSpec.describe Status, type: :model do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#mentions' do |  | ||||||
|     before do |  | ||||||
|       bob # make sure the account exists |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     it 'is empty if the status is self-contained and does not mention anyone' do |  | ||||||
|       expect(subject.mentions).to be_empty |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     it 'returns mentioned accounts' do |  | ||||||
|       subject.mentioned_accounts.create!(account: bob) |  | ||||||
|       expect(subject.mentions).to include bob |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     it 'returns account of the replied-to status' do |  | ||||||
|       subject.thread = other |  | ||||||
|       expect(subject.mentions).to include bob |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     it 'returns the account of the shared status' do |  | ||||||
|       subject.reblog = other |  | ||||||
|       expect(subject.mentions).to include bob |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   describe '#verb' do |   describe '#verb' do | ||||||
|     it 'is always post' do |     it 'is always post' do | ||||||
|       expect(subject.verb).to be :post |       expect(subject.verb).to be :post | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue