Merge tag 'v3.2.0' into hometown-dev
This commit is contained in:
@ -49,6 +49,7 @@
|
||||
# hide_collections :boolean
|
||||
# avatar_storage_schema_version :integer
|
||||
# header_storage_schema_version :integer
|
||||
# devices_url :string
|
||||
#
|
||||
|
||||
class Account < ApplicationRecord
|
||||
|
@ -108,7 +108,7 @@ class AccountConversation < ApplicationRecord
|
||||
end
|
||||
|
||||
def subscribed_to_timeline?
|
||||
Redis.current.exists("subscribed:#{streaming_channel}")
|
||||
Redis.current.exists?("subscribed:#{streaming_channel}")
|
||||
end
|
||||
|
||||
def streaming_channel
|
||||
|
20
app/models/account_note.rb
Normal file
20
app/models/account_note.rb
Normal file
@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_notes
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# target_account_id :bigint(8)
|
||||
# comment :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
class AccountNote < ApplicationRecord
|
||||
include RelationshipCacheable
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :target_account, class_name: 'Account'
|
||||
|
||||
validates :account_id, uniqueness: { scope: :target_account_id }
|
||||
end
|
@ -9,6 +9,7 @@ module AccountAssociations
|
||||
|
||||
# Identity proofs
|
||||
has_many :identity_proofs, class_name: 'AccountIdentityProof', dependent: :destroy, inverse_of: :account
|
||||
has_many :devices, dependent: :destroy, inverse_of: :account
|
||||
|
||||
# Timelines
|
||||
has_many :statuses, inverse_of: :account, dependent: :destroy
|
||||
|
@ -44,6 +44,14 @@ module AccountInteractions
|
||||
follow_mapping(AccountPin.where(account_id: account_id, target_account_id: target_account_ids), :target_account_id)
|
||||
end
|
||||
|
||||
def account_note_map(target_account_ids, account_id)
|
||||
AccountNote.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |note, mapping|
|
||||
mapping[note.target_account_id] = {
|
||||
comment: note.comment,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def domain_blocking_map(target_account_ids, account_id)
|
||||
accounts_map = Account.where(id: target_account_ids).select('id, domain').each_with_object({}) { |a, h| h[a.id] = a.domain }
|
||||
blocked_domains = domain_blocking_map_by_domain(accounts_map.values.compact, account_id)
|
||||
|
@ -8,6 +8,17 @@ module Attachmentable
|
||||
MAX_MATRIX_LIMIT = 16_777_216 # 4096x4096px or approx. 16MB
|
||||
GIF_MATRIX_LIMIT = 921_600 # 1280x720px
|
||||
|
||||
# For some file extensions, there exist different content
|
||||
# type variants, and browsers often send the wrong one,
|
||||
# for example, sending an audio .ogg file as video/ogg,
|
||||
# likewise, MimeMagic also misreports them as such. For
|
||||
# those files, it is necessary to use the output of the
|
||||
# `file` utility instead
|
||||
INCORRECT_CONTENT_TYPES = %w(
|
||||
video/ogg
|
||||
video/webm
|
||||
).freeze
|
||||
|
||||
included do
|
||||
before_post_process :obfuscate_file_name
|
||||
before_post_process :set_file_extensions
|
||||
@ -21,7 +32,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? || !INCORRECT_CONTENT_TYPES.include?(attachment.instance_read(:content_type))
|
||||
|
||||
attachment.instance_write :content_type, calculated_content_type(attachment)
|
||||
end
|
||||
@ -63,9 +74,7 @@ module Attachmentable
|
||||
end
|
||||
|
||||
def calculated_content_type(attachment)
|
||||
content_type = Paperclip.run('file', '-b --mime :file', file: attachment.queued_for_write[:original].path).split(/[:;\s]+/).first.chomp
|
||||
content_type = 'video/mp4' if content_type == 'video/x-m4v'
|
||||
content_type
|
||||
Paperclip.run('file', '-b --mime :file', file: attachment.queued_for_write[:original].path).split(/[:;\s]+/).first.chomp
|
||||
rescue Terrapin::CommandLineError
|
||||
''
|
||||
end
|
||||
|
@ -4,7 +4,7 @@ module DomainNormalizable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_save :normalize_domain
|
||||
before_validation :normalize_domain
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -57,7 +57,7 @@ module Omniauthable
|
||||
|
||||
user = User.new(user_params_from_auth(email, auth))
|
||||
|
||||
user.account.avatar_remote_url = auth.info.image if auth.info.image =~ /\A#{URI.regexp(%w(http https))}\z/
|
||||
user.account.avatar_remote_url = auth.info.image if auth.info.image =~ /\A#{URI::DEFAULT_PARSER.make_regexp(%w(http https))}\z/
|
||||
user.skip_confirmation!
|
||||
user.save!
|
||||
user
|
||||
|
@ -4,12 +4,12 @@ module Remotable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def remotable_attachment(attachment_name, limit, suppress_errors: true)
|
||||
attribute_name = "#{attachment_name}_remote_url".to_sym
|
||||
method_name = "#{attribute_name}=".to_sym
|
||||
alt_method_name = "reset_#{attachment_name}!".to_sym
|
||||
def remotable_attachment(attachment_name, limit, suppress_errors: true, download_on_assign: true, attribute_name: nil)
|
||||
attribute_name ||= "#{attachment_name}_remote_url".to_sym
|
||||
|
||||
define_method("download_#{attachment_name}!") do |url = nil|
|
||||
url ||= self[attribute_name]
|
||||
|
||||
define_method method_name do |url|
|
||||
return if url.blank?
|
||||
|
||||
begin
|
||||
@ -18,68 +18,33 @@ module Remotable
|
||||
return
|
||||
end
|
||||
|
||||
return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.blank? || (self[attribute_name] == url && send("#{attachment_name}_file_name").present?)
|
||||
return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.blank?
|
||||
|
||||
begin
|
||||
Request.new(:get, url).perform do |response|
|
||||
raise Mastodon::UnexpectedResponseError, response unless (200...300).cover?(response.code)
|
||||
|
||||
content_type = parse_content_type(response.headers.get('content-type').last)
|
||||
extname = detect_extname_from_content_type(content_type)
|
||||
|
||||
if extname.nil?
|
||||
disposition = response.headers.get('content-disposition').last
|
||||
matches = disposition&.match(/filename="([^"]*)"/)
|
||||
filename = matches.nil? ? parsed_url.path.split('/').last : matches[1]
|
||||
extname = filename.nil? ? '' : File.extname(filename)
|
||||
end
|
||||
|
||||
basename = SecureRandom.hex(8)
|
||||
|
||||
send("#{attachment_name}_file_name=", basename + extname)
|
||||
send("#{attachment_name}=", StringIO.new(response.body_with_limit(limit)))
|
||||
|
||||
self[attribute_name] = url if has_attribute?(attribute_name)
|
||||
public_send("#{attachment_name}=", ResponseWithLimit.new(response, limit))
|
||||
end
|
||||
rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError => e
|
||||
Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}"
|
||||
raise e unless suppress_errors
|
||||
rescue Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError, Paperclip::Error, Mastodon::DimensionsValidationError => e
|
||||
rescue Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError, Paperclip::Error, Mastodon::DimensionsValidationError, Mastodon::StreamValidationError => e
|
||||
Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}"
|
||||
nil
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
define_method alt_method_name do
|
||||
url = self[attribute_name]
|
||||
define_method("#{attribute_name}=") do |url|
|
||||
return if self[attribute_name] == url && public_send("#{attachment_name}_file_name").present?
|
||||
|
||||
return if url.blank?
|
||||
self[attribute_name] = url if has_attribute?(attribute_name)
|
||||
|
||||
self[attribute_name] = ''
|
||||
send(method_name, url)
|
||||
public_send("download_#{attachment_name}!", url) if download_on_assign
|
||||
end
|
||||
|
||||
alias_method("reset_#{attachment_name}!", "download_#{attachment_name}!")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def detect_extname_from_content_type(content_type)
|
||||
return if content_type.nil?
|
||||
|
||||
type = MIME::Types[content_type].first
|
||||
|
||||
return if type.nil?
|
||||
|
||||
extname = type.extensions.first
|
||||
|
||||
return if extname.nil?
|
||||
|
||||
".#{extname}"
|
||||
end
|
||||
|
||||
def parse_content_type(content_type)
|
||||
return if content_type.nil?
|
||||
|
||||
content_type.split(/\s*;\s*/).first
|
||||
end
|
||||
end
|
||||
|
35
app/models/device.rb
Normal file
35
app/models/device.rb
Normal file
@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: devices
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# access_token_id :bigint(8)
|
||||
# account_id :bigint(8)
|
||||
# device_id :string default(""), not null
|
||||
# name :string default(""), not null
|
||||
# fingerprint_key :text default(""), not null
|
||||
# identity_key :text default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class Device < ApplicationRecord
|
||||
belongs_to :access_token, class_name: 'Doorkeeper::AccessToken'
|
||||
belongs_to :account
|
||||
|
||||
has_many :one_time_keys, dependent: :destroy, inverse_of: :device
|
||||
has_many :encrypted_messages, dependent: :destroy, inverse_of: :device
|
||||
|
||||
validates :name, :fingerprint_key, :identity_key, presence: true
|
||||
validates :fingerprint_key, :identity_key, ed25519_key: true
|
||||
|
||||
before_save :invalidate_associations, if: -> { device_id_changed? || fingerprint_key_changed? || identity_key_changed? }
|
||||
|
||||
private
|
||||
|
||||
def invalidate_associations
|
||||
one_time_keys.destroy_all
|
||||
encrypted_messages.destroy_all
|
||||
end
|
||||
end
|
@ -50,11 +50,13 @@ class DomainBlock < ApplicationRecord
|
||||
def rule_for(domain)
|
||||
return if domain.blank?
|
||||
|
||||
uri = Addressable::URI.new.tap { |u| u.host = domain.gsub(/[\/]/, '') }
|
||||
uri = Addressable::URI.new.tap { |u| u.host = domain.strip.gsub(/[\/]/, '') }
|
||||
segments = uri.normalized_host.split('.')
|
||||
variants = segments.map.with_index { |_, i| segments[i..-1].join('.') }
|
||||
|
||||
where(domain: variants).order(Arel.sql('char_length(domain) desc')).first
|
||||
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
|
47
app/models/encrypted_message.rb
Normal file
47
app/models/encrypted_message.rb
Normal file
@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: encrypted_messages
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# device_id :bigint(8)
|
||||
# from_account_id :bigint(8)
|
||||
# from_device_id :string default(""), not null
|
||||
# type :integer default(0), not null
|
||||
# body :text default(""), not null
|
||||
# digest :text default(""), not null
|
||||
# message_franking :text default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class EncryptedMessage < ApplicationRecord
|
||||
self.inheritance_column = nil
|
||||
|
||||
include Paginable
|
||||
|
||||
scope :up_to, ->(id) { where(arel_table[:id].lteq(id)) }
|
||||
|
||||
belongs_to :device
|
||||
belongs_to :from_account, class_name: 'Account'
|
||||
|
||||
around_create Mastodon::Snowflake::Callbacks
|
||||
|
||||
after_commit :push_to_streaming_api
|
||||
|
||||
private
|
||||
|
||||
def push_to_streaming_api
|
||||
return if destroyed? || !subscribed_to_timeline?
|
||||
|
||||
PushEncryptedMessageWorker.perform_async(id)
|
||||
end
|
||||
|
||||
def subscribed_to_timeline?
|
||||
Redis.current.exists?("subscribed:#{streaming_channel}")
|
||||
end
|
||||
|
||||
def streaming_channel
|
||||
"timeline:#{device.account_id}:#{device.device_id}"
|
||||
end
|
||||
end
|
@ -8,6 +8,6 @@ class HomeFeed < Feed
|
||||
end
|
||||
|
||||
def regenerating?
|
||||
redis.exists("account:#{@id}:regeneration")
|
||||
redis.exists?("account:#{@id}:regeneration")
|
||||
end
|
||||
end
|
||||
|
@ -17,7 +17,7 @@
|
||||
#
|
||||
|
||||
class Import < ApplicationRecord
|
||||
FILE_TYPES = %w(text/plain text/csv).freeze
|
||||
FILE_TYPES = %w(text/plain text/csv application/csv).freeze
|
||||
MODES = %i(merge overwrite).freeze
|
||||
|
||||
self.inheritance_column = false
|
||||
|
@ -21,6 +21,11 @@
|
||||
# blurhash :string
|
||||
# processing :integer
|
||||
# file_storage_schema_version :integer
|
||||
# thumbnail_file_name :string
|
||||
# thumbnail_content_type :string
|
||||
# thumbnail_file_size :integer
|
||||
# thumbnail_updated_at :datetime
|
||||
# thumbnail_remote_url :string
|
||||
#
|
||||
|
||||
class MediaAttachment < ApplicationRecord
|
||||
@ -35,6 +40,13 @@ class MediaAttachment < ApplicationRecord
|
||||
VIDEO_FILE_EXTENSIONS = %w(.webm .mp4 .m4v .mov).freeze
|
||||
AUDIO_FILE_EXTENSIONS = %w(.ogg .oga .mp3 .wav .flac .opus .aac .m4a .3gp .wma).freeze
|
||||
|
||||
META_KEYS = %i(
|
||||
focus
|
||||
colors
|
||||
original
|
||||
small
|
||||
).freeze
|
||||
|
||||
IMAGE_MIME_TYPES = %w(image/jpeg image/png image/gif).freeze
|
||||
VIDEO_MIME_TYPES = %w(video/webm video/mp4 video/quicktime video/ogg).freeze
|
||||
VIDEO_CONVERTIBLE_MIME_TYPES = %w(video/webm video/quicktime).freeze
|
||||
@ -49,13 +61,13 @@ class MediaAttachment < ApplicationRecord
|
||||
original: {
|
||||
pixels: 1_638_400, # 1280x1280px
|
||||
file_geometry_parser: FastGeometryParser,
|
||||
},
|
||||
}.freeze,
|
||||
|
||||
small: {
|
||||
pixels: 160_000, # 400x400px
|
||||
file_geometry_parser: FastGeometryParser,
|
||||
blurhash: BLURHASH_OPTIONS,
|
||||
},
|
||||
}.freeze,
|
||||
}.freeze
|
||||
|
||||
VIDEO_FORMAT = {
|
||||
@ -74,14 +86,14 @@ class MediaAttachment < ApplicationRecord
|
||||
'frames:v' => 60 * 60 * 3,
|
||||
'crf' => 18,
|
||||
'map_metadata' => '-1',
|
||||
},
|
||||
},
|
||||
}.freeze,
|
||||
}.freeze,
|
||||
}.freeze
|
||||
|
||||
VIDEO_PASSTHROUGH_OPTIONS = {
|
||||
video_codecs: ['h264'],
|
||||
audio_codecs: ['aac', nil],
|
||||
colorspaces: ['yuv420p'],
|
||||
video_codecs: ['h264'].freeze,
|
||||
audio_codecs: ['aac', nil].freeze,
|
||||
colorspaces: ['yuv420p'].freeze,
|
||||
options: {
|
||||
format: 'mp4',
|
||||
convert_options: {
|
||||
@ -90,9 +102,9 @@ class MediaAttachment < ApplicationRecord
|
||||
'map_metadata' => '-1',
|
||||
'c:v' => 'copy',
|
||||
'c:a' => 'copy',
|
||||
},
|
||||
},
|
||||
},
|
||||
}.freeze,
|
||||
}.freeze,
|
||||
}.freeze,
|
||||
}.freeze
|
||||
|
||||
VIDEO_STYLES = {
|
||||
@ -101,15 +113,15 @@ class MediaAttachment < ApplicationRecord
|
||||
output: {
|
||||
'loglevel' => 'fatal',
|
||||
vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
|
||||
},
|
||||
},
|
||||
}.freeze,
|
||||
}.freeze,
|
||||
format: 'png',
|
||||
time: 0,
|
||||
file_geometry_parser: FastGeometryParser,
|
||||
blurhash: BLURHASH_OPTIONS,
|
||||
},
|
||||
}.freeze,
|
||||
|
||||
original: VIDEO_FORMAT.merge(passthrough_options: VIDEO_PASSTHROUGH_OPTIONS),
|
||||
original: VIDEO_FORMAT.merge(passthrough_options: VIDEO_PASSTHROUGH_OPTIONS).freeze,
|
||||
}.freeze
|
||||
|
||||
AUDIO_STYLES = {
|
||||
@ -119,16 +131,23 @@ class MediaAttachment < ApplicationRecord
|
||||
convert_options: {
|
||||
output: {
|
||||
'loglevel' => 'fatal',
|
||||
'map_metadata' => '-1',
|
||||
'q:a' => 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}.freeze,
|
||||
}.freeze,
|
||||
}.freeze,
|
||||
}.freeze
|
||||
|
||||
VIDEO_CONVERTED_STYLES = {
|
||||
small: VIDEO_STYLES[:small],
|
||||
original: VIDEO_FORMAT,
|
||||
small: VIDEO_STYLES[:small].freeze,
|
||||
original: VIDEO_FORMAT.freeze,
|
||||
}.freeze
|
||||
|
||||
THUMBNAIL_STYLES = {
|
||||
original: IMAGE_STYLES[:small].freeze,
|
||||
}.freeze
|
||||
|
||||
GLOBAL_CONVERT_OPTIONS = {
|
||||
all: '-quality 90 -strip +set modify-date +set create-date',
|
||||
}.freeze
|
||||
|
||||
IMAGE_LIMIT = 10.megabytes
|
||||
@ -144,18 +163,31 @@ class MediaAttachment < ApplicationRecord
|
||||
has_attached_file :file,
|
||||
styles: ->(f) { file_styles f },
|
||||
processors: ->(f) { file_processors f },
|
||||
convert_options: { all: '-quality 90 -strip +set modify-date +set create-date' }
|
||||
convert_options: GLOBAL_CONVERT_OPTIONS
|
||||
|
||||
before_file_post_process :set_type_and_extension
|
||||
before_file_post_process :check_video_dimensions
|
||||
|
||||
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
|
||||
validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :larger_media_format?
|
||||
validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :larger_media_format?
|
||||
remotable_attachment :file, VIDEO_LIMIT, suppress_errors: false
|
||||
remotable_attachment :file, VIDEO_LIMIT, suppress_errors: false, download_on_assign: false, attribute_name: :remote_url
|
||||
|
||||
has_attached_file :thumbnail,
|
||||
styles: THUMBNAIL_STYLES,
|
||||
processors: [:lazy_thumbnail, :blurhash_transcoder, :color_extractor],
|
||||
convert_options: GLOBAL_CONVERT_OPTIONS
|
||||
|
||||
validates_attachment_content_type :thumbnail, content_type: IMAGE_MIME_TYPES
|
||||
validates_attachment_size :thumbnail, less_than: IMAGE_LIMIT
|
||||
remotable_attachment :thumbnail, IMAGE_LIMIT, suppress_errors: true, download_on_assign: false
|
||||
|
||||
include Attachmentable
|
||||
|
||||
validates :account, presence: true
|
||||
validates :description, length: { maximum: MAX_DESCRIPTION_LENGTH }, if: :local?
|
||||
validates :file, presence: true, if: :local?
|
||||
validates :thumbnail, absence: true, if: -> { local? && !audio_or_video? }
|
||||
|
||||
scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) }
|
||||
scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) }
|
||||
@ -194,15 +226,17 @@ class MediaAttachment < ApplicationRecord
|
||||
|
||||
x, y = (point.is_a?(Enumerable) ? point : point.split(',')).map(&:to_f)
|
||||
|
||||
meta = file.instance_read(:meta) || {}
|
||||
meta = (file.instance_read(:meta) || {}).with_indifferent_access.slice(*META_KEYS)
|
||||
meta['focus'] = { 'x' => x, 'y' => y }
|
||||
|
||||
file.instance_write(:meta, meta)
|
||||
end
|
||||
|
||||
def focus
|
||||
x = file.meta['focus']['x']
|
||||
y = file.meta['focus']['y']
|
||||
x = file.meta&.dig('focus', 'x')
|
||||
y = file.meta&.dig('focus', 'y')
|
||||
|
||||
return if x.nil? || y.nil?
|
||||
|
||||
"#{x},#{y}"
|
||||
end
|
||||
@ -213,6 +247,10 @@ class MediaAttachment < ApplicationRecord
|
||||
@delay_processing
|
||||
end
|
||||
|
||||
def delay_processing_for_attachment?(attachment_name)
|
||||
@delay_processing && attachment_name == :file
|
||||
end
|
||||
|
||||
after_commit :enqueue_processing, on: :create
|
||||
after_commit :reset_parent_cache, on: :update
|
||||
|
||||
@ -220,10 +258,7 @@ class MediaAttachment < ApplicationRecord
|
||||
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
|
||||
after_post_process :set_meta
|
||||
|
||||
class << self
|
||||
def supported_mime_types
|
||||
@ -236,25 +271,25 @@ class MediaAttachment < ApplicationRecord
|
||||
|
||||
private
|
||||
|
||||
def file_styles(f)
|
||||
if f.instance.file_content_type == 'image/gif' || VIDEO_CONVERTIBLE_MIME_TYPES.include?(f.instance.file_content_type)
|
||||
def file_styles(attachment)
|
||||
if attachment.instance.file_content_type == 'image/gif' || VIDEO_CONVERTIBLE_MIME_TYPES.include?(attachment.instance.file_content_type)
|
||||
VIDEO_CONVERTED_STYLES
|
||||
elsif IMAGE_MIME_TYPES.include?(f.instance.file_content_type)
|
||||
elsif IMAGE_MIME_TYPES.include?(attachment.instance.file_content_type)
|
||||
IMAGE_STYLES
|
||||
elsif VIDEO_MIME_TYPES.include?(f.instance.file_content_type)
|
||||
elsif VIDEO_MIME_TYPES.include?(attachment.instance.file_content_type)
|
||||
VIDEO_STYLES
|
||||
else
|
||||
AUDIO_STYLES
|
||||
end
|
||||
end
|
||||
|
||||
def file_processors(f)
|
||||
if f.file_content_type == 'image/gif'
|
||||
def file_processors(instance)
|
||||
if instance.file_content_type == 'image/gif'
|
||||
[:gif_transcoder, :blurhash_transcoder]
|
||||
elsif VIDEO_MIME_TYPES.include?(f.file_content_type)
|
||||
elsif VIDEO_MIME_TYPES.include?(instance.file_content_type)
|
||||
[:video_transcoder, :blurhash_transcoder, :type_corrector]
|
||||
elsif AUDIO_MIME_TYPES.include?(f.file_content_type)
|
||||
[:transcoder, :type_corrector]
|
||||
elsif AUDIO_MIME_TYPES.include?(instance.file_content_type)
|
||||
[:image_extractor, :transcoder, :type_corrector]
|
||||
else
|
||||
[:lazy_thumbnail, :blurhash_transcoder, :type_corrector]
|
||||
end
|
||||
@ -297,29 +332,28 @@ class MediaAttachment < ApplicationRecord
|
||||
def check_video_dimensions
|
||||
return unless (video? || gifv?) && file.queued_for_write[:original].present?
|
||||
|
||||
movie = FFMPEG::Movie.new(file.queued_for_write[:original].path)
|
||||
movie = ffmpeg_data(file.queued_for_write[:original].path)
|
||||
|
||||
return unless movie.valid?
|
||||
|
||||
raise Mastodon::StreamValidationError, 'Video has no video stream' if movie.width.nil? || movie.frame_rate.nil?
|
||||
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
|
||||
|
||||
return if meta == {}
|
||||
|
||||
file.instance_write :meta, meta
|
||||
file.instance_write :meta, populate_meta
|
||||
end
|
||||
|
||||
def populate_meta
|
||||
meta = file.instance_read(:meta) || {}
|
||||
meta = (file.instance_read(:meta) || {}).with_indifferent_access.slice(*META_KEYS)
|
||||
|
||||
file.queued_for_write.each do |style, file|
|
||||
meta[style] = style == :small || image? ? image_geometry(file) : video_metadata(file)
|
||||
end
|
||||
|
||||
meta[:small] = image_geometry(thumbnail.queued_for_write[:original]) if thumbnail.queued_for_write.key?(:original)
|
||||
|
||||
meta
|
||||
end
|
||||
|
||||
@ -337,7 +371,7 @@ class MediaAttachment < ApplicationRecord
|
||||
end
|
||||
|
||||
def video_metadata(file)
|
||||
movie = FFMPEG::Movie.new(file.path)
|
||||
movie = ffmpeg_data(file.path)
|
||||
|
||||
return {} unless movie.valid?
|
||||
|
||||
@ -350,6 +384,13 @@ class MediaAttachment < ApplicationRecord
|
||||
}.compact
|
||||
end
|
||||
|
||||
# We call this method about 3 different times on potentially different
|
||||
# paths but ultimately the same file, so it makes sense to memoize the
|
||||
# result while disregarding the path
|
||||
def ffmpeg_data(path = nil)
|
||||
@ffmpeg_data ||= FFMPEG::Movie.new(path)
|
||||
end
|
||||
|
||||
def enqueue_processing
|
||||
PostProcessMediaWorker.perform_async(id) if delay_processing?
|
||||
end
|
||||
|
19
app/models/message_franking.rb
Normal file
19
app/models/message_franking.rb
Normal file
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class MessageFranking
|
||||
attr_reader :hmac, :source_account_id, :target_account_id,
|
||||
:timestamp, :original_franking
|
||||
|
||||
def initialize(attributes = {})
|
||||
@hmac = attributes[:hmac]
|
||||
@source_account_id = attributes[:source_account_id]
|
||||
@target_account_id = attributes[:target_account_id]
|
||||
@timestamp = attributes[:timestamp]
|
||||
@original_franking = attributes[:original_franking]
|
||||
end
|
||||
|
||||
def to_token
|
||||
crypt = ActiveSupport::MessageEncryptor.new(SystemKey.current_key, serializer: Oj)
|
||||
crypt.encrypt_and_sign(self)
|
||||
end
|
||||
end
|
21
app/models/one_time_key.rb
Normal file
21
app/models/one_time_key.rb
Normal file
@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: one_time_keys
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# device_id :bigint(8)
|
||||
# key_id :string default(""), not null
|
||||
# key :text default(""), not null
|
||||
# signature :text default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class OneTimeKey < ApplicationRecord
|
||||
belongs_to :device
|
||||
|
||||
validates :key_id, :key, :signature, presence: true
|
||||
validates :key, ed25519_key: true
|
||||
validates :signature, ed25519_signature: { message: :key, verify_key: ->(one_time_key) { one_time_key.device.fingerprint_key } }
|
||||
end
|
@ -23,19 +23,25 @@
|
||||
# updated_at :datetime not null
|
||||
# embed_url :string default(""), not null
|
||||
# image_storage_schema_version :integer
|
||||
# blurhash :string
|
||||
#
|
||||
|
||||
class PreviewCard < ApplicationRecord
|
||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
|
||||
LIMIT = 1.megabytes
|
||||
|
||||
BLURHASH_OPTIONS = {
|
||||
x_comp: 4,
|
||||
y_comp: 4,
|
||||
}.freeze
|
||||
|
||||
self.inheritance_column = false
|
||||
|
||||
enum type: [:link, :photo, :video, :rich]
|
||||
|
||||
has_and_belongs_to_many :statuses
|
||||
|
||||
has_attached_file :image, styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }
|
||||
has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }
|
||||
|
||||
include Attachmentable
|
||||
|
||||
@ -72,6 +78,7 @@ class PreviewCard < ApplicationRecord
|
||||
geometry: '400x400>',
|
||||
file_geometry_parser: FastGeometryParser,
|
||||
convert_options: '-coalesce -strip',
|
||||
blurhash: BLURHASH_OPTIONS,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -142,7 +142,7 @@ class Status < ApplicationRecord
|
||||
ids << account_id if local?
|
||||
|
||||
if preloaded.nil?
|
||||
ids += mentions.where(account: Account.local).pluck(:account_id)
|
||||
ids += mentions.where(account: Account.local, silent: false).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)
|
||||
|
41
app/models/system_key.rb
Normal file
41
app/models/system_key.rb
Normal file
@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: system_keys
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# key :binary
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
class SystemKey < ApplicationRecord
|
||||
ROTATION_PERIOD = 1.week.freeze
|
||||
|
||||
before_validation :set_key
|
||||
|
||||
scope :expired, ->(now = Time.now.utc) { where(arel_table[:created_at].lt(now - ROTATION_PERIOD * 3)) }
|
||||
|
||||
class << self
|
||||
def current_key
|
||||
previous_key = order(id: :asc).last
|
||||
|
||||
if previous_key && previous_key.created_at >= ROTATION_PERIOD.ago
|
||||
previous_key.key
|
||||
else
|
||||
create.key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_key
|
||||
return if key.present?
|
||||
|
||||
cipher = OpenSSL::Cipher.new('AES-256-GCM')
|
||||
cipher.encrypt
|
||||
|
||||
self.key = cipher.random_key
|
||||
end
|
||||
end
|
@ -38,6 +38,8 @@
|
||||
# chosen_languages :string is an Array
|
||||
# created_by_application_id :bigint(8)
|
||||
# approved :boolean default(TRUE), not null
|
||||
# sign_in_token :string
|
||||
# sign_in_token_sent_at :datetime
|
||||
#
|
||||
|
||||
class User < ApplicationRecord
|
||||
@ -113,7 +115,7 @@ class User < ApplicationRecord
|
||||
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images, :default_federation,
|
||||
to: :settings, prefix: :setting, allow_nil: false
|
||||
|
||||
attr_reader :invite_code
|
||||
attr_reader :invite_code, :sign_in_token_attempt
|
||||
attr_writer :external
|
||||
|
||||
def confirmed?
|
||||
@ -167,6 +169,10 @@ class User < ApplicationRecord
|
||||
true
|
||||
end
|
||||
|
||||
def suspicious_sign_in?(ip)
|
||||
!otp_required_for_login? && current_sign_in_at.present? && current_sign_in_at < 2.weeks.ago && !recent_ip?(ip)
|
||||
end
|
||||
|
||||
def functional?
|
||||
confirmed? && approved? && !disabled? && !account.suspended? && account.moved_to_account_id.nil?
|
||||
end
|
||||
@ -269,6 +275,13 @@ class User < ApplicationRecord
|
||||
super
|
||||
end
|
||||
|
||||
def external_or_valid_password?(compare_password)
|
||||
# If encrypted_password is blank, we got the user from LDAP or PAM,
|
||||
# so credentials are already valid
|
||||
|
||||
encrypted_password.blank? || valid_password?(compare_password)
|
||||
end
|
||||
|
||||
def send_reset_password_instructions
|
||||
return false if encrypted_password.blank?
|
||||
|
||||
@ -304,6 +317,15 @@ class User < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def sign_in_token_expired?
|
||||
sign_in_token_sent_at.nil? || sign_in_token_sent_at < 5.minutes.ago
|
||||
end
|
||||
|
||||
def generate_sign_in_token
|
||||
self.sign_in_token = Devise.friendly_token(6)
|
||||
self.sign_in_token_sent_at = Time.now.utc
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def send_devise_notification(notification, *args)
|
||||
@ -320,6 +342,10 @@ class User < ApplicationRecord
|
||||
|
||||
private
|
||||
|
||||
def recent_ip?(ip)
|
||||
recent_ips.any? { |(_, recent_ip)| recent_ip == ip }
|
||||
end
|
||||
|
||||
def send_pending_devise_notifications
|
||||
pending_devise_notifications.each do |notification, args|
|
||||
render_and_send_devise_message(notification, *args)
|
||||
|
Reference in New Issue
Block a user