diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/account.rb | 16 | ||||
-rw-r--r-- | app/models/account_statuses_filter.rb | 2 | ||||
-rw-r--r-- | app/models/concerns/has_user_settings.rb | 36 | ||||
-rw-r--r-- | app/models/concerns/status_snapshot_concern.rb | 1 | ||||
-rw-r--r-- | app/models/custom_emoji.rb | 7 | ||||
-rw-r--r-- | app/models/direct_feed.rb | 31 | ||||
-rw-r--r-- | app/models/form/admin_settings.rb | 29 | ||||
-rw-r--r-- | app/models/media_attachment.rb | 4 | ||||
-rw-r--r-- | app/models/public_feed.rb | 14 | ||||
-rw-r--r-- | app/models/status.rb | 62 | ||||
-rw-r--r-- | app/models/status_edit.rb | 1 | ||||
-rw-r--r-- | app/models/tag_feed.rb | 1 | ||||
-rw-r--r-- | app/models/trends.rb | 9 | ||||
-rw-r--r-- | app/models/trends/statuses.rb | 2 | ||||
-rw-r--r-- | app/models/user.rb | 3 | ||||
-rw-r--r-- | app/models/user_settings.rb | 9 |
16 files changed, 203 insertions, 24 deletions
diff --git a/app/models/account.rb b/app/models/account.rb index f49cae901..4fc7b9d08 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -79,6 +79,10 @@ class Account < ApplicationRecord include DomainMaterializable include AccountMerging + MAX_DISPLAY_NAME_LENGTH = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i + MAX_NOTE_LENGTH = (ENV['MAX_BIO_CHARS'] || 500).to_i + DEFAULT_FIELDS_SIZE = (ENV['MAX_PROFILE_FIELDS'] || 4).to_i + enum protocol: { ostatus: 0, activitypub: 1 } enum suspension_origin: { local: 0, remote: 1 }, _prefix: true @@ -91,9 +95,9 @@ class Account < ApplicationRecord # Local user validations validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? && actor_type != 'Application' } validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && actor_type != 'Application' } - validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? } - validates :note, note_length: { maximum: 500 }, if: -> { local? && will_save_change_to_note? } - validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? } + validates :display_name, length: { maximum: MAX_DISPLAY_NAME_LENGTH }, if: -> { local? && will_save_change_to_display_name? } + validates :note, note_length: { maximum: MAX_NOTE_LENGTH }, if: -> { local? && will_save_change_to_note? } + validates :fields, length: { maximum: DEFAULT_FIELDS_SIZE }, if: -> { local? && will_save_change_to_fields? } scope :remote, -> { where.not(domain: nil) } scope :local, -> { where(domain: nil) } @@ -259,10 +263,6 @@ class Account < ApplicationRecord update!(memorial: true) end - def trendable? - boolean_with_default('trendable', Setting.trendable_by_default) - end - def sign? true end @@ -325,8 +325,6 @@ class Account < ApplicationRecord self[:fields] = fields end - DEFAULT_FIELDS_SIZE = 4 - def build_fields return if fields.size >= DEFAULT_FIELDS_SIZE diff --git a/app/models/account_statuses_filter.rb b/app/models/account_statuses_filter.rb index 211f41478..556aee032 100644 --- a/app/models/account_statuses_filter.rb +++ b/app/models/account_statuses_filter.rb @@ -35,7 +35,7 @@ class AccountStatusesFilter if suspended? Status.none elsif anonymous? - account.statuses.where(visibility: %i(public unlisted)) + account.statuses.not_local_only.where(visibility: %i(public unlisted)) elsif author? account.statuses.all # NOTE: #merge! does not work without the #all elsif blocked? diff --git a/app/models/concerns/has_user_settings.rb b/app/models/concerns/has_user_settings.rb index b3fa1f683..0e9d4e1cd 100644 --- a/app/models/concerns/has_user_settings.rb +++ b/app/models/concerns/has_user_settings.rb @@ -39,6 +39,10 @@ module HasUserSettings settings['web.delete_modal'] end + def setting_favourite_modal + settings['web.favourite_modal'] + end + def setting_reduce_motion settings['web.reduce_motion'] end @@ -47,12 +51,20 @@ module HasUserSettings settings['web.use_system_font'] end + def setting_system_emoji_font + settings['web.use_system_emoji_font'] + end + def setting_noindex settings['noindex'] end - def setting_theme - settings['theme'] + def setting_flavour + settings['flavour'] + end + + def setting_skin + settings['skin'] end def setting_display_media @@ -107,6 +119,14 @@ module HasUserSettings settings['default_privacy'] || (account.locked? ? 'private' : 'public') end + def setting_default_content_type + settings['default_content_type'] + end + + def setting_hide_followers_count + settings['hide_followers_count'] + end + def allows_report_emails? settings['notification_emails.report'] end @@ -123,6 +143,18 @@ module HasUserSettings settings['notification_emails.trends'] end + def allows_trending_tags_review_emails? + settings['notification_emails.trends'] + end + + def allows_trending_links_review_emails? + settings['notification_emails.link_trends'] + end + + def allows_trending_statuses_review_emails? + settings['notification_emails.status_trends'] + end + def aggregates_reblogs? settings['aggregate_reblogs'] end diff --git a/app/models/concerns/status_snapshot_concern.rb b/app/models/concerns/status_snapshot_concern.rb index 9741b9aeb..c728db7c3 100644 --- a/app/models/concerns/status_snapshot_concern.rb +++ b/app/models/concerns/status_snapshot_concern.rb @@ -24,6 +24,7 @@ module StatusSnapshotConcern media_descriptions: ordered_media_attachments.map(&:description), poll_options: preloadable_poll&.options&.dup, account_id: account_id || self.account_id, + content_type: content_type, created_at: at_time || edited_at, rate_limit: rate_limit ) diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 3d7900226..b5a07a5a0 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -24,7 +24,8 @@ class CustomEmoji < ApplicationRecord include Attachmentable - LIMIT = 256.kilobytes + LOCAL_LIMIT = (ENV['MAX_EMOJI_SIZE'] || 256.kilobytes).to_i + LIMIT = [LOCAL_LIMIT, (ENV['MAX_REMOTE_EMOJI_SIZE'] || 256.kilobytes).to_i].max SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}' @@ -42,7 +43,9 @@ class CustomEmoji < ApplicationRecord before_validation :downcase_domain - validates_attachment :image, content_type: { content_type: IMAGE_MIME_TYPES }, presence: true, size: { less_than: LIMIT } + validates_attachment :image, content_type: { content_type: IMAGE_MIME_TYPES }, presence: true + validates_attachment_size :image, less_than: LIMIT, unless: :local? + validates_attachment_size :image, less_than: LOCAL_LIMIT, if: :local? validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: 2 } scope :local, -> { where(domain: nil) } diff --git a/app/models/direct_feed.rb b/app/models/direct_feed.rb new file mode 100644 index 000000000..1f2448070 --- /dev/null +++ b/app/models/direct_feed.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class DirectFeed < Feed + include Redisable + + def initialize(account) + @type = :direct + @id = account.id + @account = account + end + + def get(limit, max_id = nil, since_id = nil, min_id = nil) + unless redis.exists("account:#{@account.id}:regeneration") + statuses = super + return statuses unless statuses.empty? + end + from_database(limit, max_id, since_id, min_id) + end + + private + + def from_database(limit, max_id, since_id, min_id) + loop do + statuses = Status.as_direct_timeline(@account, limit, max_id, since_id, min_id) + return statuses if statuses.empty? + max_id = statuses.last.id + statuses = statuses.reject { |status| FeedManager.instance.filter?(:direct, status, @account) } + return statuses unless statuses.empty? + end + end +end diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index de965cb0b..eaee142fa 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -14,21 +14,29 @@ class Form::AdminSettings closed_registrations_message timeline_preview bootstrap_timeline_accounts - theme + flavour + skin activity_api_enabled peers_api_enabled preview_sensitive_media custom_css profile_directory + hide_followers_count + flavour_and_skin thumbnail mascot + show_reblogs_in_public_timelines + show_replies_in_public_timelines trends trends_as_landing_page trendable_by_default + trending_status_cw show_domain_blocks show_domain_blocks_rationale noindex + outgoing_spoilers require_invite_text + captcha_enabled media_cache_retention_period content_cache_retention_period backups_retention_period @@ -47,11 +55,16 @@ class Form::AdminSettings peers_api_enabled preview_sensitive_media profile_directory + hide_followers_count + show_reblogs_in_public_timelines + show_replies_in_public_timelines trends trends_as_landing_page trendable_by_default + trending_status_cw noindex require_invite_text + captcha_enabled ).freeze UPLOAD_KEYS = %i( @@ -59,6 +72,10 @@ class Form::AdminSettings mascot ).freeze + PSEUDO_KEYS = %i( + flavour_and_skin + ).freeze + attr_accessor(*KEYS) validates :registrations_mode, inclusion: { in: %w(open approved none) }, if: -> { defined?(@registrations_mode) } @@ -102,7 +119,7 @@ class Form::AdminSettings return false unless errors.empty? && valid? KEYS.each do |key| - next unless instance_variable_defined?("@#{key}") + next if PSEUDO_KEYS.include?(key) || !instance_variable_defined?("@#{key}") if UPLOAD_KEYS.include?(key) public_send(key).save @@ -113,6 +130,14 @@ class Form::AdminSettings end end + def flavour_and_skin + "#{Setting.flavour}/#{Setting.skin}" + end + + def flavour_and_skin=(value) + @flavour, @skin = value.split('/', 2) + end + private def typecast_value(key, value) diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index e51e13b95..0367b4af7 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -39,8 +39,8 @@ class MediaAttachment < ApplicationRecord MAX_DESCRIPTION_LENGTH = 1_500 - IMAGE_LIMIT = 16.megabytes - VIDEO_LIMIT = 99.megabytes + IMAGE_LIMIT = (ENV['MAX_IMAGE_SIZE'] || 16.megabytes).to_i + VIDEO_LIMIT = (ENV['MAX_VIDEO_SIZE'] || 99.megabytes).to_i MAX_VIDEO_MATRIX_LIMIT = 8_294_400 # 3840x2160px MAX_VIDEO_FRAME_RATE = 120 diff --git a/app/models/public_feed.rb b/app/models/public_feed.rb index 1cfd9a500..a987bb72c 100644 --- a/app/models/public_feed.rb +++ b/app/models/public_feed.rb @@ -8,6 +8,7 @@ class PublicFeed # @option [Boolean] :local # @option [Boolean] :remote # @option [Boolean] :only_media + # @option [Boolean] :allow_local_only def initialize(account, options = {}) @account = account @options = options @@ -21,6 +22,7 @@ class PublicFeed def get(limit, max_id = nil, since_id = nil, min_id = nil) scope = public_scope + scope.merge!(without_local_only_scope) unless allow_local_only? scope.merge!(without_replies_scope) unless with_replies? scope.merge!(without_reblogs_scope) unless with_reblogs? scope.merge!(local_only_scope) if local_only? @@ -36,6 +38,10 @@ class PublicFeed attr_reader :account, :options + def allow_local_only? + local_account? && (local_only? || options[:allow_local_only]) + end + def with_reblogs? options[:with_reblogs] end @@ -56,6 +62,10 @@ class PublicFeed account.present? end + def local_account? + account&.local? + end + def media_only? options[:only_media] end @@ -84,6 +94,10 @@ class PublicFeed Status.joins(:media_attachments).group(:id) end + def without_local_only_scope + Status.not_local_only + end + def language_scope Status.where(language: account.chosen_languages) end diff --git a/app/models/status.rb b/app/models/status.rb index 2757497db..e01ddb5c5 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -22,7 +22,9 @@ # account_id :bigint(8) not null # application_id :bigint(8) # in_reply_to_account_id :bigint(8) +# local_only :boolean # poll_id :bigint(8) +# content_type :string # deleted_at :datetime # edited_at :datetime # trendable :boolean @@ -85,6 +87,7 @@ class Status < ApplicationRecord validates_with DisallowedHashtagsValidator validates :reblog, uniqueness: { scope: :account }, if: :reblog? validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog? + validates :content_type, inclusion: { in: %w(text/plain text/markdown text/html) }, allow_nil: true accepts_nested_attributes_for :poll @@ -111,6 +114,8 @@ class Status < ApplicationRecord where('NOT EXISTS (SELECT * FROM statuses_tags forbidden WHERE forbidden.status_id = statuses.id AND forbidden.tag_id IN (?))', tag_ids) } + scope :not_local_only, -> { where(local_only: [false, nil]) } + after_create_commit :trigger_create_webhooks after_update_commit :trigger_update_webhooks @@ -322,6 +327,7 @@ class Status < ApplicationRecord before_validation :set_visibility before_validation :set_conversation before_validation :set_local + before_create :set_locality around_create Mastodon::Snowflake::Callbacks @@ -332,6 +338,47 @@ class Status < ApplicationRecord visibilities.keys - %w(direct limited) end + 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 queries 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 + + 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 favourites_map(status_ids, account_id) Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true } end @@ -387,6 +434,15 @@ class Status < ApplicationRecord end end + def marked_local_only? + # match both with and without U+FE0F (the emoji variation selector) + /#{local_only_emoji}\ufe0f?\z/.match?(content) + end + + def local_only_emoji + '👁' + end + def status_stat super || build_status_stat end @@ -496,6 +552,12 @@ class Status < ApplicationRecord self.sensitive = false if sensitive.nil? end + def set_locality + if account.domain.nil? && !attribute_changed?(:local_only) + self.local_only = marked_local_only? + end + end + def set_conversation self.thread = thread.reblog if thread&.reblog? diff --git a/app/models/status_edit.rb b/app/models/status_edit.rb index 2b3248bb2..fa35e38ac 100644 --- a/app/models/status_edit.rb +++ b/app/models/status_edit.rb @@ -11,6 +11,7 @@ # spoiler_text :text default(""), not null # created_at :datetime not null # updated_at :datetime not null +# content_type :string # ordered_media_attachment_ids :bigint(8) is an Array # media_descriptions :text is an Array # poll_options :string is an Array diff --git a/app/models/tag_feed.rb b/app/models/tag_feed.rb index b8cd63557..fbbdbaae2 100644 --- a/app/models/tag_feed.rb +++ b/app/models/tag_feed.rb @@ -25,6 +25,7 @@ class TagFeed < PublicFeed def get(limit, max_id = nil, since_id = nil, min_id = nil) scope = public_scope + scope.merge!(without_local_only_scope) unless local_account? scope.merge!(tagged_with_any_scope) scope.merge!(tagged_with_all_scope) scope.merge!(tagged_with_none_scope) diff --git a/app/models/trends.rb b/app/models/trends.rb index d07d62b71..b09db940e 100644 --- a/app/models/trends.rb +++ b/app/models/trends.rb @@ -32,10 +32,13 @@ module Trends tags_requiring_review = tags.request_review statuses_requiring_review = statuses.request_review - return if links_requiring_review.empty? && tags_requiring_review.empty? && statuses_requiring_review.empty? - User.those_who_can(:manage_taxonomies).includes(:account).find_each do |user| - AdminMailer.new_trends(user.account, links_requiring_review, tags_requiring_review, statuses_requiring_review).deliver_later! if user.allows_trends_review_emails? + links = user.allows_trending_links_review_emails? ? links_requiring_review : [] + tags = user.allows_trending_tags_review_emails? ? tags_requiring_review : [] + statuses = user.allows_trending_statuses_review_emails? ? statuses_requiring_review : [] + next if links.empty? && tags.empty? && statuses.empty? + + AdminMailer.new_trends(user.account, links, tags, statuses).deliver_later! end end diff --git a/app/models/trends/statuses.rb b/app/models/trends/statuses.rb index 84bff9c02..d4d5b1c24 100644 --- a/app/models/trends/statuses.rb +++ b/app/models/trends/statuses.rb @@ -91,7 +91,7 @@ class Trends::Statuses < Trends::Base private def eligible?(status) - status.public_visibility? && status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? && valid_locale?(status.language) + status.public_visibility? && status.account.discoverable? && !status.account.silenced? && (status.spoiler_text.blank? || Setting.trending_status_cw) && !status.sensitive? && !status.reply? && valid_locale?(status.language) end def calculate_scores(statuses, at_time) diff --git a/app/models/user.rb b/app/models/user.rb index 9b225d75f..3471bb2c1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -244,7 +244,8 @@ class User < ApplicationRecord end def functional? - functional_or_moved? && account.moved_to_account_id.nil? + + functional_or_moved? end def functional_or_moved? diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index 2c025d6c5..0be8c5fbc 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -9,12 +9,15 @@ class UserSettings setting :always_send_emails, default: false setting :aggregate_reblogs, default: true - setting :theme, default: -> { ::Setting.theme } + setting :flavour, default: -> { ::Setting.flavour } + setting :skin, default: -> { ::Setting.skin } setting :noindex, default: -> { ::Setting.noindex } setting :show_application, default: true setting :default_language, default: nil setting :default_sensitive, default: false setting :default_privacy, default: nil + setting :default_content_type, default: 'text/plain' + setting :hide_followers_count, default: false namespace :web do setting :crop_images, default: true @@ -27,10 +30,12 @@ class UserSettings setting :delete_modal, default: true setting :reblog_modal, default: false setting :unfollow_modal, default: true + setting :favourite_modal, default: false setting :reduce_motion, default: false setting :expand_content_warnings, default: false setting :display_media, default: 'default', in: %w(default show_all hide_all) setting :auto_play, default: false + setting :use_system_emoji_font, default: false end namespace :notification_emails do @@ -42,6 +47,8 @@ class UserSettings setting :report, default: true setting :pending_account, default: true setting :trends, default: true + setting :link_trends, default: false + setting :status_trends, default: false setting :appeal, default: true end |