Merge tag 'v3.1.4' into hometown-dev

This commit is contained in:
Darius Kazemi
2020-05-15 15:34:04 -07:00
1182 changed files with 15087 additions and 6745 deletions

View File

@ -3,49 +3,52 @@
#
# Table name: accounts
#
# id :bigint(8) not null, primary key
# username :string default(""), not null
# domain :string
# secret :string default(""), not null
# private_key :text
# public_key :text default(""), not null
# remote_url :string default(""), not null
# salmon_url :string default(""), not null
# hub_url :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# note :text default(""), not null
# display_name :string default(""), not null
# uri :string default(""), not null
# url :string
# avatar_file_name :string
# avatar_content_type :string
# avatar_file_size :integer
# avatar_updated_at :datetime
# header_file_name :string
# header_content_type :string
# header_file_size :integer
# header_updated_at :datetime
# avatar_remote_url :string
# subscription_expires_at :datetime
# locked :boolean default(FALSE), not null
# header_remote_url :string default(""), not null
# last_webfingered_at :datetime
# inbox_url :string default(""), not null
# outbox_url :string default(""), not null
# shared_inbox_url :string default(""), not null
# followers_url :string default(""), not null
# protocol :integer default("ostatus"), not null
# memorial :boolean default(FALSE), not null
# moved_to_account_id :bigint(8)
# featured_collection_url :string
# fields :jsonb
# actor_type :string
# discoverable :boolean
# also_known_as :string is an Array
# silenced_at :datetime
# suspended_at :datetime
# trust_level :integer
# id :bigint(8) not null, primary key
# username :string default(""), not null
# domain :string
# secret :string default(""), not null
# private_key :text
# public_key :text default(""), not null
# remote_url :string default(""), not null
# salmon_url :string default(""), not null
# hub_url :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# note :text default(""), not null
# display_name :string default(""), not null
# uri :string default(""), not null
# url :string
# avatar_file_name :string
# avatar_content_type :string
# avatar_file_size :integer
# avatar_updated_at :datetime
# header_file_name :string
# header_content_type :string
# header_file_size :integer
# header_updated_at :datetime
# avatar_remote_url :string
# subscription_expires_at :datetime
# locked :boolean default(FALSE), not null
# header_remote_url :string default(""), not null
# last_webfingered_at :datetime
# inbox_url :string default(""), not null
# outbox_url :string default(""), not null
# shared_inbox_url :string default(""), not null
# followers_url :string default(""), not null
# protocol :integer default("ostatus"), not null
# memorial :boolean default(FALSE), not null
# moved_to_account_id :bigint(8)
# featured_collection_url :string
# fields :jsonb
# actor_type :string
# discoverable :boolean
# also_known_as :string is an Array
# silenced_at :datetime
# suspended_at :datetime
# trust_level :integer
# hide_collections :boolean
# avatar_storage_schema_version :integer
# header_storage_schema_version :integer
#
class Account < ApplicationRecord
@ -102,6 +105,7 @@ class Account < ApplicationRecord
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
scope :popular, -> { order('account_stats.followers_count desc') }
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
@ -322,6 +326,14 @@ class Account < ApplicationRecord
save!
end
def hides_followers?
hide_collections? || user_hides_network?
end
def hides_following?
hide_collections? || user_hides_network?
end
def object_type
:person
end
@ -403,7 +415,7 @@ class Account < ApplicationRecord
def inboxes
urls = reorder(nil).where(protocol: :activitypub).pluck(Arel.sql("distinct coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url)"))
DeliveryFailureTracker.filter(urls)
DeliveryFailureTracker.without_unavailable(urls)
end
def search_for(terms, limit = 10, offset = 0)
@ -478,7 +490,16 @@ class Account < ApplicationRecord
def from_text(text)
return [] if text.blank?
text.scan(MENTION_RE).map { |match| match.first.split('@', 2) }.uniq.map { |(username, domain)| EntityCache.instance.mention(username, domain) }
text.scan(MENTION_RE).map { |match| match.first.split('@', 2) }.uniq.map do |(username, domain)|
domain = begin
if TagManager.instance.local_domain?(domain)
nil
else
TagManager.instance.normalize_domain(domain)
end
end
EntityCache.instance.mention(username, domain)
end.compact
end
private

View File

@ -16,8 +16,8 @@ class AccountAlias < ApplicationRecord
belongs_to :account
validates :acct, presence: true, domain: { acct: true }
validates :uri, presence: true
validates :uri, uniqueness: { scope: :account_id }
validate :validate_target_account
before_validation :set_uri
after_create :add_to_account
@ -44,4 +44,12 @@ class AccountAlias < ApplicationRecord
def remove_from_account
account.update(also_known_as: account.also_known_as.reject { |x| x == uri })
end
def validate_target_account
if uri.blank?
errors.add(:acct, I18n.t('migrations.errors.not_found'))
elsif ActivityPub::TagManager.instance.uri_for(account) == uri
errors.add(:acct, I18n.t('migrations.errors.move_to_self'))
end
end
end

View File

@ -14,6 +14,7 @@ class AccountFilter
email
ip
staff
order
).freeze
attr_reader :params
@ -24,7 +25,7 @@ class AccountFilter
end
def results
scope = Account.recent.includes(:user)
scope = Account.includes(:user).reorder(nil)
params.each do |key, value|
scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
@ -38,6 +39,7 @@ class AccountFilter
def set_defaults!
params['local'] = '1' if params['remote'].blank?
params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank? && params['pending'].blank?
params['order'] = 'recent' if params['order'].blank?
end
def scope_for(key, value)
@ -51,9 +53,9 @@ class AccountFilter
when 'active'
Account.without_suspended
when 'pending'
accounts_with_users.merge User.pending
accounts_with_users.merge(User.pending)
when 'disabled'
accounts_with_users.merge User.disabled
accounts_with_users.merge(User.disabled)
when 'silenced'
Account.silenced
when 'suspended'
@ -63,16 +65,31 @@ class AccountFilter
when 'display_name'
Account.matches_display_name(value)
when 'email'
accounts_with_users.merge User.matches_email(value)
accounts_with_users.merge(User.matches_email(value))
when 'ip'
valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none
when 'staff'
accounts_with_users.merge User.staff
accounts_with_users.merge(User.staff)
when 'order'
order_scope(value)
else
raise "Unknown filter: #{key}"
end
end
def order_scope(value)
case value
when 'active'
params['remote'] ? Account.joins(:account_stat).by_recent_status : Account.joins(:user).by_recent_sign_in
when 'recent'
Account.recent
when 'alphabetic'
Account.alphabetic
else
raise "Unknown order: #{value}"
end
end
def accounts_with_users
Account.joins(:user)
end

View File

@ -8,8 +8,11 @@
# text :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# title :string default(""), not null
#
class AccountWarningPreset < ApplicationRecord
validates :text, presence: true
scope :alphabetic, -> { order(title: :asc, text: :asc) }
end

View File

@ -62,8 +62,6 @@ class Admin::AccountAction
def process_action!
case type
when 'none'
handle_resolve!
when 'disable'
handle_disable!
when 'silence'
@ -105,16 +103,6 @@ class Admin::AccountAction
end
end
def handle_resolve!
if with_report? && report.account_id == -99 && target_account.trust_level == Account::TRUST_LEVELS[:untrusted]
# This is an automated report and it is being dismissed, so it's
# a false positive, in which case update the account's trust level
# to prevent further spam checks
target_account.update(trust_level: Account::TRUST_LEVELS[:trusted])
end
end
def handle_disable!
authorize(target_account.user, :disable?)
log_action(:disable, target_account.user)

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
class Admin::ActionLogFilter
KEYS = %i(
action_type
account_id
target_account_id
).freeze
ACTION_TYPE_MAP = {
assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
change_email_user: { target_type: 'User', action: 'change_email' }.freeze,
confirm_user: { target_type: 'User', action: 'confirm' }.freeze,
create_account_warning: { target_type: 'AccountWarning', action: 'create' }.freeze,
create_announcement: { target_type: 'Announcement', action: 'create' }.freeze,
create_custom_emoji: { target_type: 'CustomEmoji', action: 'create' }.freeze,
create_domain_allow: { target_type: 'DomainAllow', action: 'create' }.freeze,
create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze,
create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze,
demote_user: { target_type: 'User', action: 'demote' }.freeze,
destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze,
destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze,
destroy_domain_allow: { target_type: 'DomainAllow', action: 'destroy' }.freeze,
destroy_domain_block: { target_type: 'DomainBlock', action: 'destroy' }.freeze,
destroy_email_domain_block: { target_type: 'EmailDomainBlock', action: 'destroy' }.freeze,
destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze,
disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,
disable_user: { target_type: 'User', action: 'disable' }.freeze,
enable_custom_emoji: { target_type: 'CustomEmoji', action: 'enable' }.freeze,
enable_user: { target_type: 'User', action: 'enable' }.freeze,
memorialize_account: { target_type: 'Account', action: 'memorialize' }.freeze,
promote_user: { target_type: 'User', action: 'promote' }.freeze,
remove_avatar_user: { target_type: 'User', action: 'remove_avatar' }.freeze,
reopen_report: { target_type: 'Report', action: 'reopen' }.freeze,
reset_password_user: { target_type: 'User', action: 'reset_password' }.freeze,
resolve_report: { target_type: 'Report', action: 'resolve' }.freeze,
silence_account: { target_type: 'Account', action: 'silence' }.freeze,
suspend_account: { target_type: 'Account', action: 'suspend' }.freeze,
unassigned_report: { target_type: 'Report', action: 'unassigned' }.freeze,
unsilence_account: { target_type: 'Account', action: 'unsilence' }.freeze,
unsuspend_account: { target_type: 'Account', action: 'unsuspend' }.freeze,
update_announcement: { target_type: 'Announcement', action: 'update' }.freeze,
update_custom_emoji: { target_type: 'CustomEmoji', action: 'update' }.freeze,
update_status: { target_type: 'Status', action: 'update' }.freeze,
}.freeze
attr_reader :params
def initialize(params)
@params = params
end
def results
scope = Admin::ActionLog.includes(:target)
params.each do |key, value|
next if key.to_s == 'page'
scope.merge!(scope_for(key.to_s, value.to_s.strip)) if value.present?
end
scope
end
private
def scope_for(key, value)
case key
when 'action_type'
Admin::ActionLog.where(ACTION_TYPE_MAP[value.to_sym])
when 'account_id'
Admin::ActionLog.where(account_id: value)
when 'target_account_id'
account = Account.find(value)
Admin::ActionLog.where(target: [account, account.user].compact)
else
raise "Unknown filter: #{key}"
end
end
end

View File

@ -14,6 +14,7 @@
# created_at :datetime not null
# updated_at :datetime not null
# published_at :datetime
# status_ids :bigint(8) is an Array
#
class Announcement < ApplicationRecord
@ -48,6 +49,16 @@ class Announcement < ApplicationRecord
@mentions ||= Account.from_text(text)
end
def statuses
@statuses ||= begin
if status_ids.nil?
[]
else
Status.where(id: status_ids, visibility: [:public, :unlisted])
end
end
end
def tags
@tags ||= Tag.find_or_create_by_names(Extractor.extract_hashtags(text))
end

View File

@ -87,10 +87,10 @@ module AccountInteractions
has_many :announcement_mutes, dependent: :destroy
end
def follow!(other_account, reblogs: nil, uri: nil)
def follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
reblogs = true if reblogs.nil?
rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri)
rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
.find_or_create_by!(target_account: other_account)
rel.update!(show_reblogs: reblogs)
@ -99,6 +99,18 @@ module AccountInteractions
rel
end
def request_follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
reblogs = true if reblogs.nil?
rel = follow_requests.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
.find_or_create_by!(target_account: other_account)
rel.update!(show_reblogs: reblogs)
remove_potential_friendship(other_account)
rel
end
def block!(other_account, uri: nil)
remove_potential_friendship(other_account)
block_relationships.create_with(uri: uri)

View File

@ -74,7 +74,7 @@ module Attachmentable
self.class.attachment_definitions.each_key do |attachment_name|
attachment = send(attachment_name)
next if attachment.blank? || attachment.queued_for_write[:original].blank?
next if attachment.blank? || attachment.queued_for_write[:original].blank? || attachment.options[:preserve_files]
attachment.instance_write :file_name, SecureRandom.hex(8) + File.extname(attachment.instance_read(:file_name))
end

View File

@ -82,7 +82,7 @@ module Omniauthable
username = starting_username
i = 0
while Account.exists?(username: username)
while Account.exists?(username: username, domain: nil)
i += 1
username = "#{starting_username}_#{i}"
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module RateLimitable
extend ActiveSupport::Concern
def rate_limit=(value)
@rate_limit = value
end
def rate_limit?
@rate_limit
end
def rate_limiter(by, options = {})
return @rate_limiter if defined?(@rate_limiter)
@rate_limiter = RateLimiter.new(by, options)
end
class_methods do
def rate_limit(options = {})
after_create do
by = public_send(options[:by])
if rate_limit? && by&.local?
rate_limiter(by, options).record!
@rate_limit_recorded = true
end
end
after_rollback do
rate_limiter(public_send(options[:by]), options).rollback! if @rate_limit_recorded
end
end
end
end

View File

@ -3,20 +3,21 @@
#
# Table name: custom_emojis
#
# id :bigint(8) not null, primary key
# shortcode :string default(""), not null
# domain :string
# image_file_name :string
# image_content_type :string
# image_file_size :integer
# image_updated_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# disabled :boolean default(FALSE), not null
# uri :string
# image_remote_url :string
# visible_in_picker :boolean default(TRUE), not null
# category_id :bigint(8)
# id :bigint(8) not null, primary key
# shortcode :string default(""), not null
# domain :string
# image_file_name :string
# image_content_type :string
# image_file_size :integer
# image_updated_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# disabled :boolean default(FALSE), not null
# uri :string
# image_remote_url :string
# visible_in_picker :boolean default(TRUE), not null
# category_id :bigint(8)
# image_storage_schema_version :integer
#
class CustomEmoji < ApplicationRecord

View File

@ -7,13 +7,27 @@
# domain :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# parent_id :bigint(8)
#
class EmailDomainBlock < ApplicationRecord
include DomainNormalizable
belongs_to :parent, class_name: 'EmailDomainBlock', optional: true
has_many :children, class_name: 'EmailDomainBlock', foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
validates :domain, presence: true, uniqueness: true, domain: true
def with_dns_records=(val)
@with_dns_records = ActiveModel::Type::Boolean.new.cast(val)
end
def with_dns_records?
@with_dns_records
end
alias with_dns_records with_dns_records?
def self.block?(email)
_, domain = email.split('@', 2)

View File

@ -15,6 +15,9 @@
class Follow < ApplicationRecord
include Paginable
include RelationshipCacheable
include RateLimitable
rate_limit by: :account, family: :follows
belongs_to :account
belongs_to :target_account, class_name: 'Account'

View File

@ -15,6 +15,9 @@
class FollowRequest < ApplicationRecord
include Paginable
include RelationshipCacheable
include RateLimitable
rate_limit by: :account, family: :follows
belongs_to :account
belongs_to :target_account, class_name: 'Account'

View File

@ -3,28 +3,31 @@
#
# Table name: media_attachments
#
# id :bigint(8) not null, primary key
# status_id :bigint(8)
# file_file_name :string
# file_content_type :string
# file_file_size :integer
# file_updated_at :datetime
# remote_url :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# shortcode :string
# type :integer default("image"), not null
# file_meta :json
# account_id :bigint(8)
# description :text
# scheduled_status_id :bigint(8)
# blurhash :string
# id :bigint(8) not null, primary key
# status_id :bigint(8)
# file_file_name :string
# file_content_type :string
# file_file_size :integer
# file_updated_at :datetime
# remote_url :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# shortcode :string
# type :integer default("image"), not null
# file_meta :json
# account_id :bigint(8)
# description :text
# scheduled_status_id :bigint(8)
# blurhash :string
# processing :integer
# file_storage_schema_version :integer
#
class MediaAttachment < ApplicationRecord
self.inheritance_column = nil
enum type: [:image, :gifv, :video, :unknown, :audio]
enum processing: [:queued, :in_progress, :complete, :failed], _prefix: true
MAX_DESCRIPTION_LENGTH = 1_500
@ -55,47 +58,6 @@ class MediaAttachment < ApplicationRecord
},
}.freeze
VIDEO_STYLES = {
small: {
convert_options: {
output: {
'loglevel' => 'fatal',
vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
},
},
format: 'png',
time: 0,
file_geometry_parser: FastGeometryParser,
blurhash: BLURHASH_OPTIONS,
},
original: {
keep_same_format: true,
convert_options: {
output: {
'loglevel' => 'fatal',
'map_metadata' => '-1',
'c:v' => 'copy',
'c:a' => 'copy',
},
},
},
}.freeze
AUDIO_STYLES = {
original: {
format: 'mp3',
content_type: 'audio/mpeg',
convert_options: {
output: {
'loglevel' => 'fatal',
'map_metadata' => '-1',
'q:a' => 2,
},
},
},
}.freeze
VIDEO_FORMAT = {
format: 'mp4',
content_type: 'video/mp4',
@ -116,6 +78,54 @@ class MediaAttachment < ApplicationRecord
},
}.freeze
VIDEO_PASSTHROUGH_OPTIONS = {
video_codecs: ['h264'],
audio_codecs: ['aac', nil],
colorspaces: ['yuv420p'],
options: {
format: 'mp4',
convert_options: {
output: {
'loglevel' => 'fatal',
'map_metadata' => '-1',
'c:v' => 'copy',
'c:a' => 'copy',
},
},
},
}.freeze
VIDEO_STYLES = {
small: {
convert_options: {
output: {
'loglevel' => 'fatal',
vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
},
},
format: 'png',
time: 0,
file_geometry_parser: FastGeometryParser,
blurhash: BLURHASH_OPTIONS,
},
original: VIDEO_FORMAT.merge(passthrough_options: VIDEO_PASSTHROUGH_OPTIONS),
}.freeze
AUDIO_STYLES = {
original: {
format: 'mp3',
content_type: 'audio/mpeg',
convert_options: {
output: {
'loglevel' => 'fatal',
'map_metadata' => '-1',
'q:a' => 2,
},
},
},
}.freeze
VIDEO_CONVERTED_STYLES = {
small: VIDEO_STYLES[:small],
original: VIDEO_FORMAT,
@ -124,6 +134,9 @@ class MediaAttachment < ApplicationRecord
IMAGE_LIMIT = 10.megabytes
VIDEO_LIMIT = 40.megabytes
MAX_VIDEO_MATRIX_LIMIT = 2_304_000 # 1920x1200px
MAX_VIDEO_FRAME_RATE = 60
belongs_to :account, inverse_of: :media_attachments, optional: true
belongs_to :status, inverse_of: :media_attachments, optional: true
belongs_to :scheduled_status, inverse_of: :media_attachments, optional: true
@ -156,6 +169,10 @@ class MediaAttachment < ApplicationRecord
remote_url.blank?
end
def not_processed?
processing.present? && !processing_complete?
end
def needs_redownload?
file.blank? && remote_url.present?
end
@ -168,18 +185,6 @@ class MediaAttachment < ApplicationRecord
audio? || video?
end
def variant?(other_file_name)
return true if file_file_name == other_file_name
formats = file.styles.values.map(&:format).compact
return false if formats.empty?
extension = File.extname(other_file_name)
formats.include?(extension.delete('.')) && File.basename(other_file_name, extension) == File.basename(file_file_name, File.extname(file_file_name))
end
def to_param
shortcode
end
@ -202,12 +207,21 @@ class MediaAttachment < ApplicationRecord
"#{x},#{y}"
end
attr_writer :delay_processing
def delay_processing?
@delay_processing
end
after_commit :enqueue_processing, on: :create
after_commit :reset_parent_cache, on: :update
before_create :prepare_description, unless: :local?
before_create :set_shortcode
before_create :set_processing
before_post_process :set_type_and_extension
before_post_process :check_video_dimensions
before_save :set_meta
@ -276,6 +290,21 @@ class MediaAttachment < ApplicationRecord
end
end
def set_processing
self.processing = delay_processing? ? :queued : :complete
end
def check_video_dimensions
return unless (video? || gifv?) && file.queued_for_write[:original].present?
movie = FFMPEG::Movie.new(file.queued_for_write[:original].path)
return unless movie.valid?
raise Mastodon::DimensionsValidationError, "#{movie.width}x#{movie.height} videos are not supported" if movie.width * movie.height > MAX_VIDEO_MATRIX_LIMIT
raise Mastodon::DimensionsValidationError, "#{movie.frame_rate.to_i}fps videos are not supported" if movie.frame_rate > MAX_VIDEO_FRAME_RATE
end
def set_meta
meta = populate_meta
@ -321,9 +350,11 @@ class MediaAttachment < ApplicationRecord
}.compact
end
def reset_parent_cache
return if status_id.nil?
def enqueue_processing
PostProcessMediaWorker.perform_async(id) if delay_processing?
end
Rails.cache.delete("statuses/#{status_id}")
def reset_parent_cache
Rails.cache.delete("statuses/#{status_id}") if status_id.present?
end
end

View File

@ -3,25 +3,26 @@
#
# Table name: preview_cards
#
# id :bigint(8) not null, primary key
# url :string default(""), not null
# title :string default(""), not null
# description :string default(""), not null
# image_file_name :string
# image_content_type :string
# image_file_size :integer
# image_updated_at :datetime
# type :integer default("link"), not null
# html :text default(""), not null
# author_name :string default(""), not null
# author_url :string default(""), not null
# provider_name :string default(""), not null
# provider_url :string default(""), not null
# width :integer default(0), not null
# height :integer default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
# embed_url :string default(""), not null
# id :bigint(8) not null, primary key
# url :string default(""), not null
# title :string default(""), not null
# description :string default(""), not null
# image_file_name :string
# image_content_type :string
# image_file_size :integer
# image_updated_at :datetime
# type :integer default("link"), not null
# html :text default(""), not null
# author_name :string default(""), not null
# author_url :string default(""), not null
# provider_name :string default(""), not null
# provider_url :string default(""), not null
# width :integer default(0), not null
# height :integer default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
# embed_url :string default(""), not null
# image_storage_schema_version :integer
#
class PreviewCard < ApplicationRecord
@ -47,6 +48,10 @@ class PreviewCard < ApplicationRecord
before_save :extract_dimensions, if: :link?
def local?
false
end
def missing_image?
width.present? && height.present? && image_file_name.blank?
end

View File

@ -23,7 +23,7 @@ class RelationshipFilter
scope = scope_for('relationship', params['relationship'].to_s.strip)
params.each do |key, value|
next if key.to_s == 'page'
next if %w(relationship page).include?(key)
scope.merge!(scope_for(key.to_s, value.to_s.strip)) if value.present?
end

View File

@ -27,7 +27,7 @@ class Relay < ApplicationRecord
payload = Oj.dump(follow_activity(activity_id))
update!(state: :pending, follow_activity_id: activity_id)
DeliveryFailureTracker.new(inbox_url).track_success!
DeliveryFailureTracker.reset!(inbox_url)
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
end
@ -36,7 +36,7 @@ class Relay < ApplicationRecord
payload = Oj.dump(unfollow_activity(activity_id))
update!(state: :idle, follow_activity_id: nil)
DeliveryFailureTracker.new(inbox_url).track_success!
DeliveryFailureTracker.reset!(inbox_url)
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
end

View File

@ -3,6 +3,7 @@
class RemoteFollow
include ActiveModel::Validations
include RoutingHelper
include WebfingerHelper
attr_accessor :acct, :addressable_template
@ -71,7 +72,7 @@ class RemoteFollow
end
def acct_resource
@acct_resource ||= Goldfinger.finger("acct:#{acct}")
@acct_resource ||= webfinger!("acct:#{acct}")
rescue Goldfinger::Error, HTTP::ConnectionError
nil
end

View File

@ -18,6 +18,9 @@
class Report < ApplicationRecord
include Paginable
include RateLimitable
rate_limit by: :account, family: :reports
belongs_to :account
belongs_to :target_account, class_name: 'Account'
@ -59,6 +62,14 @@ class Report < ApplicationRecord
end
def resolve!(acting_account)
if account_id == -99 && target_account.trust_level == Account::TRUST_LEVELS[:untrusted]
# This is an automated report and it is being dismissed, so it's
# a false positive, in which case update the account's trust level
# to prevent further spam checks
target_account.update(trust_level: Account::TRUST_LEVELS[:trusted])
end
RemovalWorker.push_bulk(Status.with_discarded.discarded.where(id: status_ids).pluck(:id)) { |status_id| [status_id, { immediate: true }] }
update!(action_taken: true, action_taken_by_account_id: acting_account.id)
end

View File

@ -34,6 +34,9 @@ class Status < ApplicationRecord
include Paginable
include Cacheable
include StatusThreadingConcern
include RateLimitable
rate_limit by: :account, family: :statuses
self.discard_column = :deleted_at
@ -142,10 +145,12 @@ class Status < ApplicationRecord
ids += mentions.where(account: Account.local).pluck(:account_id)
ids += favourites.where(account: Account.local).pluck(:account_id)
ids += reblogs.where(account: Account.local).pluck(:account_id)
ids += bookmarks.where(account: Account.local).pluck(:account_id)
else
ids += preloaded.mentions[id] || []
ids += preloaded.favourites[id] || []
ids += preloaded.reblogs[id] || []
ids += preloaded.bookmarks[id] || []
end
ids.uniq
@ -199,18 +204,6 @@ class Status < ApplicationRecord
preview_cards.first
end
def title
if destroyed?
"#{account.acct} deleted status"
elsif reblog?
preview = sensitive ? '<sensitive>' : text.slice(0, 10).split("\n")[0]
"#{account.acct} shared #{reblog.account.acct}'s: #{preview}"
else
preview = sensitive ? '<sensitive>' : text.slice(0, 20).split("\n")[0]
"#{account.acct}: #{preview}"
end
end
def hidden?
!distributable?
end
@ -300,7 +293,7 @@ class Status < ApplicationRecord
def as_public_timeline(account = nil, local_only = false)
query = timeline_scope(local_only).without_replies
apply_timeline_filters(query, account, local_only)
apply_timeline_filters(query, account, [:local, true].include?(local_only))
end
def as_tag_timeline(tag, account = nil, local_only = false)
@ -358,7 +351,7 @@ class Status < ApplicationRecord
if account.nil?
where(visibility: visibility).without_local_only
elsif target_account.blocking?(account) # get rid of blocked peeps
elsif target_account.blocking?(account) || (account.domain.present? && target_account.domain_blocking?(account.domain)) # get rid of blocked peeps
none
elsif account.id == target_account.id # author can see own stuff
all
@ -375,10 +368,33 @@ class Status < ApplicationRecord
end
end
def from_text(text)
return [] if text.blank?
text.scan(FetchLinkCardService::URL_PATTERN).map(&:first).uniq.map do |url|
status = begin
if TagManager.instance.local_url?(url)
ActivityPub::TagManager.instance.uri_to_resource(url, Status)
else
EntityCache.instance.status(url)
end
end
status&.distributable? ? status : nil
end.compact
end
private
def timeline_scope(local_only = false)
starting_scope = local_only ? Status.local : Status
def timeline_scope(scope = false)
starting_scope = case scope
when :local, true
Status.local
when :remote
Status.remote
else
Status
end
starting_scope
.with_public_visibility
.without_reblogs

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: unavailable_domains
#
# id :bigint(8) not null, primary key
# domain :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class UnavailableDomain < ApplicationRecord
include DomainNormalizable
after_commit :reset_cache!
private
def reset_cache!
Rails.cache.delete('unavailable_domains')
end
end

View File

@ -98,6 +98,7 @@ class User < ApplicationRecord
before_validation :sanitize_languages
before_create :set_approved
after_commit :send_pending_devise_notifications
# This avoids a deprecation warning from Rails 5.1
# It seems possible that a future release of devise-two-factor will
@ -306,11 +307,38 @@ class User < ApplicationRecord
protected
def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
# This method can be called in `after_update` and `after_commit` hooks,
# but we must make sure the mailer is actually called *after* commit,
# otherwise it may work on stale data. To do this, figure out if we are
# within a transaction.
if ActiveRecord::Base.connection.current_transaction.try(:records)&.include?(self)
pending_devise_notifications << [notification, args]
else
render_and_send_devise_message(notification, *args)
end
end
private
def send_pending_devise_notifications
pending_devise_notifications.each do |notification, args|
render_and_send_devise_message(notification, *args)
end
# Empty the pending notifications array because the
# after_commit hook can be called multiple times which
# could cause multiple emails to be sent.
pending_devise_notifications.clear
end
def pending_devise_notifications
@pending_devise_notifications ||= []
end
def render_and_send_devise_message(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
end
def set_approved
self.approved = open_registrations? || valid_invitation? || external?
end

View File

@ -94,11 +94,11 @@ class Web::PushSubscription < ApplicationRecord
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 push'),
Doorkeeper.configuration.access_token_expires_in,
Doorkeeper.configuration.refresh_token_enabled?
application: Doorkeeper::Application.find_by(superapp: true),
resource_owner: session_activation.user_id,
scopes: Doorkeeper::OAuth::Scopes.from_string('read write follow push'),
expires_in: Doorkeeper.configuration.access_token_expires_in,
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?
)
end
end