diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/account.rb | 31 | ||||
-rw-r--r-- | app/models/block.rb | 10 | ||||
-rw-r--r-- | app/models/concerns/account_interactions.rb | 13 | ||||
-rw-r--r-- | app/models/concerns/remotable.rb | 3 | ||||
-rw-r--r-- | app/models/concerns/status_threading_concern.rb | 23 | ||||
-rw-r--r-- | app/models/follow.rb | 13 | ||||
-rw-r--r-- | app/models/follow_request.rb | 16 | ||||
-rw-r--r-- | app/models/status.rb | 43 | ||||
-rw-r--r-- | app/models/tag.rb | 16 | ||||
-rw-r--r-- | app/models/trending_tags.rb | 61 | ||||
-rw-r--r-- | app/models/user.rb | 11 | ||||
-rw-r--r-- | app/models/web/push_subscription.rb | 73 |
12 files changed, 257 insertions, 56 deletions
diff --git a/app/models/account.rb b/app/models/account.rb index c1ce1e99e..48f284785 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -45,6 +45,7 @@ # moved_to_account_id :bigint(8) # featured_collection_url :string # fields :jsonb +# actor_type :string # class Account < ApplicationRecord @@ -76,6 +77,7 @@ class Account < ApplicationRecord validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? } validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? } validate :note_length_does_not_exceed_length_limit, if: -> { local? && will_save_change_to_note? } + validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? } # Timelines has_many :stream_entries, inverse_of: :account, dependent: :destroy @@ -137,6 +139,7 @@ class Account < ApplicationRecord :moderator?, :staff?, :locale, + :hides_network?, to: :user, prefix: true, allow_nil: true @@ -151,6 +154,16 @@ class Account < ApplicationRecord moved_to_account_id.present? end + def bot? + %w(Application Service).include? actor_type + end + + alias bot bot? + + def bot=(val) + self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person' + end + def acct local? ? username : "#{username}@#{domain}" end @@ -201,9 +214,11 @@ class Account < ApplicationRecord def fields_attributes=(attributes) fields = [] - attributes.each_value do |attr| - next if attr[:name].blank? - fields << attr + if attributes.is_a?(Hash) + attributes.each_value do |attr| + next if attr[:name].blank? + fields << attr + end end self[:fields] = fields @@ -272,8 +287,8 @@ class Account < ApplicationRecord def initialize(account, attr) @account = account - @name = attr['name'] - @value = attr['value'] + @name = attr['name'].strip[0, 255] + @value = attr['value'].strip[0, 255] @errors = {} end @@ -398,7 +413,7 @@ class Account < ApplicationRecord end def emojis - @emojis ||= CustomEmoji.from_text(note, domain) + @emojis ||= CustomEmoji.from_text(emojifiable_text, domain) end before_create :generate_keys @@ -441,4 +456,8 @@ class Account < ApplicationRecord self.domain = TagManager.instance.normalize_domain(domain) end + + def emojifiable_text + [note, display_name, fields.map(&:value)].join(' ') + end end diff --git a/app/models/block.rb b/app/models/block.rb index df4a6bbac..bf3e07600 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -8,6 +8,7 @@ # updated_at :datetime not null # account_id :bigint(8) not null # target_account_id :bigint(8) not null +# uri :string # class Block < ApplicationRecord @@ -19,7 +20,12 @@ class Block < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } + def local? + false # Force uri_for to use uri attribute + end + after_commit :remove_blocking_cache + before_validation :set_uri, only: :create private @@ -27,4 +33,8 @@ class Block < ApplicationRecord Rails.cache.delete("exclude_account_ids_for:#{account_id}") Rails.cache.delete("exclude_account_ids_for:#{target_account_id}") end + + def set_uri + self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? + end end diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 20fc74ba6..a064248d9 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -82,16 +82,19 @@ module AccountInteractions has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy end - def follow!(other_account, reblogs: nil) + def follow!(other_account, reblogs: nil, uri: nil) reblogs = true if reblogs.nil? - rel = active_relationships.create_with(show_reblogs: reblogs).find_or_create_by!(target_account: other_account) - rel.update!(show_reblogs: reblogs) + rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri) + .find_or_create_by!(target_account: other_account) + + rel.update!(show_reblogs: reblogs) rel end - def block!(other_account) - block_relationships.find_or_create_by!(target_account: other_account) + def block!(other_account, uri: nil) + block_relationships.create_with(uri: uri) + .find_or_create_by!(target_account: other_account) end def mute!(other_account, notifications: nil) diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index 7f1ef5191..c17f04776 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -41,6 +41,9 @@ module Remotable rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}" nil + rescue Paperclip::Error, Mastodon::DimensionsValidationError => e + Rails.logger.debug "Error processing remote #{attachment_name}: #{e}" + nil end end diff --git a/app/models/concerns/status_threading_concern.rb b/app/models/concerns/status_threading_concern.rb index 8e817be00..1ba8fc693 100644 --- a/app/models/concerns/status_threading_concern.rb +++ b/app/models/concerns/status_threading_concern.rb @@ -74,16 +74,7 @@ module StatusThreadingConcern statuses = statuses_with_accounts(ids).to_a account_ids = statuses.map(&:account_id).uniq domains = statuses.map(&:account_domain).compact.uniq - - relations = if account.present? - { - blocking: Account.blocking_map(account_ids, account.id), - blocked_by: Account.blocked_by_map(account_ids, account.id), - muting: Account.muting_map(account_ids, account.id), - following: Account.following_map(account_ids, account.id), - domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id), - } - end + relations = relations_map_for_account(account, account_ids, domains) statuses.reject! { |status| filter_from_context?(status, account, relations) } @@ -91,6 +82,18 @@ module StatusThreadingConcern statuses.sort_by! { |status| ids.index(status.id) } end + def relations_map_for_account(account, account_ids, domains) + return {} if account.nil? + + { + blocking: Account.blocking_map(account_ids, account.id), + blocked_by: Account.blocked_by_map(account_ids, account.id), + muting: Account.muting_map(account_ids, account.id), + following: Account.following_map(account_ids, account.id), + domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id), + } + end + def statuses_with_accounts(ids) Status.where(id: ids).includes(:account) end diff --git a/app/models/follow.rb b/app/models/follow.rb index 2ca42ff70..eaf8445f3 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -9,6 +9,7 @@ # account_id :bigint(8) not null # target_account_id :bigint(8) not null # show_reblogs :boolean default(TRUE), not null +# uri :string # class Follow < ApplicationRecord @@ -26,4 +27,16 @@ class Follow < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } scope :recent, -> { reorder(id: :desc) } + + def local? + false # Force uri_for to use uri attribute + end + + before_validation :set_uri, only: :create + + private + + def set_uri + self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? + end end diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index d559a8f62..9c4875564 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -9,6 +9,7 @@ # account_id :bigint(8) not null # target_account_id :bigint(8) not null # show_reblogs :boolean default(TRUE), not null +# uri :string # class FollowRequest < ApplicationRecord @@ -23,11 +24,22 @@ class FollowRequest < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } def authorize! - account.follow!(target_account, reblogs: show_reblogs) + account.follow!(target_account, reblogs: show_reblogs, uri: uri) MergeWorker.perform_async(target_account.id, account.id) - destroy! end alias reject! destroy! + + def local? + false # Force uri_for to use uri attribute + end + + before_validation :set_uri, only: :create + + private + + def set_uri + self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? + end end diff --git a/app/models/status.rb b/app/models/status.rb index 0b3a7c0aa..c6d6453df 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -195,12 +195,45 @@ class Status < ApplicationRecord where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private]) end - def as_direct_timeline(account) - query = joins("LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{account.id}") - .where("mentions.account_id = #{account.id} OR statuses.account_id = #{account.id}") - .where(visibility: [:direct]) + def as_direct_timeline(account, limit = 20, max_id = nil, since_id = nil, cache_ids = false) + # direct timeline is mix of direct message from_me and to_me. + # 2 querys are executed with pagination. + # constant expression using arel_table is required for partial index + + # _from_me part does not require any timeline filters + query_from_me = where(account_id: account.id) + .where(Status.arel_table[:visibility].eq(3)) + .limit(limit) + .order('statuses.id DESC') + + # _to_me part requires mute and block filter. + # FIXME: may we check mutes.hide_notifications? + query_to_me = Status + .joins(:mentions) + .merge(Mention.where(account_id: account.id)) + .where(Status.arel_table[:visibility].eq(3)) + .limit(limit) + .order('mentions.status_id DESC') + .not_excluded_by_account(account) + + if max_id.present? + query_from_me = query_from_me.where('statuses.id < ?', max_id) + query_to_me = query_to_me.where('mentions.status_id < ?', max_id) + end + + if since_id.present? + query_from_me = query_from_me.where('statuses.id > ?', since_id) + query_to_me = query_to_me.where('mentions.status_id > ?', since_id) + end - apply_timeline_filters(query, account, false) + if cache_ids + # returns array of cache_ids object that have id and updated_at + (query_from_me.cache_ids.to_a + query_to_me.cache_ids.to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) + else + # returns ActiveRecord.Relation + items = (query_from_me.select(:id).to_a + query_to_me.select(:id).to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) + Status.where(id: items.map(&:id)) + end end def as_public_timeline(account = nil, local_only = false) diff --git a/app/models/tag.rb b/app/models/tag.rb index 8b1b02412..4f31f796e 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -21,6 +21,22 @@ class Tag < ApplicationRecord name end + def history + days = [] + + 7.times do |i| + day = i.days.ago.beginning_of_day.to_i + + days << { + day: day.to_s, + uses: Redis.current.get("activity:tags:#{id}:#{day}") || '0', + accounts: Redis.current.pfcount("activity:tags:#{id}:#{day}:accounts").to_s, + } + end + + days + end + class << self def search_for(term, limit = 5) pattern = sanitize_sql_like(term.strip) + '%' diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb new file mode 100644 index 000000000..eedd92644 --- /dev/null +++ b/app/models/trending_tags.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class TrendingTags + KEY = 'trending_tags' + HALF_LIFE = 1.day.to_i + MAX_ITEMS = 500 + EXPIRE_HISTORY_AFTER = 7.days.seconds + + class << self + def record_use!(tag, account, at_time = Time.now.utc) + return if disallowed_hashtags.include?(tag.name) || account.silenced? + + increment_vote!(tag.id, at_time) + increment_historical_use!(tag.id, at_time) + increment_unique_use!(tag.id, account.id, at_time) + end + + def get(limit) + tag_ids = redis.zrevrange(KEY, 0, limit).map(&:to_i) + tags = Tag.where(id: tag_ids).to_a.map { |tag| [tag.id, tag] }.to_h + tag_ids.map { |tag_id| tags[tag_id] }.compact + end + + private + + def increment_vote!(tag_id, at_time) + redis.zincrby(KEY, (2**((at_time.to_i - epoch) / HALF_LIFE)).to_f, tag_id.to_s) + redis.zremrangebyrank(KEY, 0, -MAX_ITEMS) if rand < (2.to_f / MAX_ITEMS) + end + + def increment_historical_use!(tag_id, at_time) + key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}" + redis.incrby(key, 1) + redis.expire(key, EXPIRE_HISTORY_AFTER) + end + + def increment_unique_use!(tag_id, account_id, at_time) + key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}:accounts" + redis.pfadd(key, account_id) + redis.expire(key, EXPIRE_HISTORY_AFTER) + end + + # The epoch needs to be 2.5 years in the future if the half-life is one day + # While dynamic, it will always be the same within one year + def epoch + @epoch ||= Date.new(Date.current.year + 2.5, 10, 1).to_datetime.to_i + end + + def disallowed_hashtags + return @disallowed_hashtags if defined?(@disallowed_hashtags) + + @disallowed_hashtags = Setting.disallowed_hashtags.nil? ? [] : Setting.disallowed_hashtags + @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String + @disallowed_hashtags = @disallowed_hashtags.map(&:downcase) + end + + def redis + Redis.current + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 24beb77b2..ef48282fd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -41,7 +41,7 @@ class User < ApplicationRecord include Settings::Extend include Omniauthable - ACTIVE_DURATION = 14.days + ACTIVE_DURATION = 7.days devise :two_factor_authenticatable, otp_secret_encryption_key: Rails.configuration.x.otp_secret @@ -65,6 +65,7 @@ class User < ApplicationRecord validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? validates_with BlacklistedEmailValidator, if: :email_changed? + validates_with EmailMxValidator, if: :email_changed? scope :recent, -> { order(id: :desc) } scope :admins, -> { where(admin: true) } @@ -86,7 +87,7 @@ class User < ApplicationRecord has_many :session_activations, dependent: :destroy delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal, - :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_sensitive_media, + :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_sensitive_media, :hide_network, to: :settings, prefix: :setting, allow_nil: false attr_accessor :invite_code @@ -219,6 +220,10 @@ class User < ApplicationRecord settings.notification_emails['digest'] end + def hides_network? + @hides_network ||= settings.hide_network + end + def token_for_app(a) return nil if a.nil? || a.owner != self Doorkeeper::AccessToken @@ -245,7 +250,7 @@ class User < ApplicationRecord end def web_push_subscription(session) - session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload + session.web_push_subscription.nil? ? nil : session.web_push_subscription end def invite_code=(code) diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index 1736106f7..867bc9519 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -3,46 +3,65 @@ # # Table name: web_push_subscriptions # -# id :bigint(8) not null, primary key -# endpoint :string not null -# key_p256dh :string not null -# key_auth :string not null -# data :json -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint(8) not null, primary key +# endpoint :string not null +# key_p256dh :string not null +# key_auth :string not null +# data :json +# created_at :datetime not null +# updated_at :datetime not null +# access_token_id :bigint(8) +# user_id :bigint(8) # -require 'webpush' - class Web::PushSubscription < ApplicationRecord + belongs_to :user, optional: true + belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', optional: true + has_one :session_activation def push(notification) - I18n.with_locale(session_activation.user.locale || I18n.default_locale) do - push_payload(message_from(notification), 48.hours.seconds) + I18n.with_locale(associated_user&.locale || I18n.default_locale) do + push_payload(payload_for_notification(notification), 48.hours.seconds) end end def pushable?(notification) - data&.key?('alerts') && data['alerts'][notification.type.to_s] + data&.key?('alerts') && ActiveModel::Type::Boolean.new.cast(data['alerts'][notification.type.to_s]) end - def as_payload - payload = { id: id, endpoint: endpoint } - payload[:alerts] = data['alerts'] if data&.key?('alerts') - payload + def associated_user + return @associated_user if defined?(@associated_user) + + @associated_user = if user_id.nil? + session_activation.user + else + user + end end - def access_token - find_or_create_access_token.token + def associated_access_token + return @associated_access_token if defined?(@associated_access_token) + + @associated_access_token = if access_token_id.nil? + find_or_create_access_token.token + else + access_token.token + end + end + + class << self + def unsubscribe_for(application_id, resource_owner) + access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id, revoked_at: nil) + .pluck(:id) + + where(access_token_id: access_token_ids).delete_all + end end private def push_payload(message, ttl = 5.minutes.seconds) - # TODO: Make sure that the payload does not - # exceed 4KB - Webpush::PayloadTooLarge - Webpush.payload_send( message: Oj.dump(message), endpoint: endpoint, @@ -57,16 +76,20 @@ class Web::PushSubscription < ApplicationRecord ) end - def message_from(notification) - serializable_resource = ActiveModelSerializers::SerializableResource.new(notification, serializer: Web::NotificationSerializer, scope: self, scope_name: :current_push_subscription) - serializable_resource.as_json + def payload_for_notification(notification) + ActiveModelSerializers::SerializableResource.new( + notification, + serializer: Web::NotificationSerializer, + scope: self, + scope_name: :current_push_subscription + ).as_json end def find_or_create_access_token Doorkeeper::AccessToken.find_or_create_for( Doorkeeper::Application.find_by(superapp: true), session_activation.user_id, - Doorkeeper::OAuth::Scopes.from_string('read write follow'), + Doorkeeper::OAuth::Scopes.from_string('read write follow push'), Doorkeeper.configuration.access_token_expires_in, Doorkeeper.configuration.refresh_token_enabled? ) |