Merge tootsuite/master at 3023725936
This commit is contained in:
@@ -54,6 +54,8 @@ class Account < ApplicationRecord
|
||||
include Attachmentable
|
||||
include Remotable
|
||||
|
||||
MAX_NOTE_LENGTH = 500
|
||||
|
||||
enum protocol: [:ostatus, :activitypub]
|
||||
|
||||
# Local users
|
||||
@@ -68,7 +70,7 @@ class Account < ApplicationRecord
|
||||
validates :username, format: { with: /\A[a-z0-9_]+\z/i }, uniqueness: { scope: :domain, case_sensitive: false }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? }
|
||||
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? }
|
||||
validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? }
|
||||
validates :note, length: { maximum: 160 }, if: -> { local? && will_save_change_to_note? }
|
||||
validate :note_length_does_not_exceed_length_limit, if: -> { local? && will_save_change_to_note? }
|
||||
|
||||
# Timelines
|
||||
has_many :stream_entries, inverse_of: :account, dependent: :destroy
|
||||
@@ -307,6 +309,22 @@ class Account < ApplicationRecord
|
||||
self.public_key = keypair.public_key.to_pem
|
||||
end
|
||||
|
||||
YAML_START = "---\r\n"
|
||||
YAML_END = "\r\n...\r\n"
|
||||
|
||||
def note_length_does_not_exceed_length_limit
|
||||
note_without_metadata = note
|
||||
if note.start_with? YAML_START
|
||||
idx = note.index YAML_END
|
||||
unless idx.nil?
|
||||
note_without_metadata = note[(idx + YAML_END.length) .. -1]
|
||||
end
|
||||
end
|
||||
if note_without_metadata.mb_chars.grapheme_length > MAX_NOTE_LENGTH
|
||||
errors.add(:note, "can't be longer than 500 graphemes")
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_domain
|
||||
return if local?
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ module AccountInteractions
|
||||
|
||||
class_methods do
|
||||
def following_map(target_account_ids, account_id)
|
||||
follow_mapping(Follow.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
|
||||
Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
|
||||
mapping[follow.target_account_id] = {
|
||||
reblogs: follow.show_reblogs?
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def followed_by_map(target_account_ids, account_id)
|
||||
@@ -25,7 +29,11 @@ module AccountInteractions
|
||||
end
|
||||
|
||||
def requested_map(target_account_ids, account_id)
|
||||
follow_mapping(FollowRequest.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
|
||||
FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
|
||||
mapping[follow_request.target_account_id] = {
|
||||
reblogs: follow_request.show_reblogs?
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def domain_blocking_map(target_account_ids, account_id)
|
||||
@@ -66,8 +74,12 @@ module AccountInteractions
|
||||
has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy
|
||||
end
|
||||
|
||||
def follow!(other_account)
|
||||
active_relationships.find_or_create_by!(target_account: other_account)
|
||||
def follow!(other_account, reblogs: 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
|
||||
end
|
||||
|
||||
def block!(other_account)
|
||||
@@ -140,6 +152,10 @@ module AccountInteractions
|
||||
mute_relationships.where(target_account: other_account, hide_notifications: true).exists?
|
||||
end
|
||||
|
||||
def muting_reblogs?(other_account)
|
||||
active_relationships.where(target_account: other_account, show_reblogs: false).exists?
|
||||
end
|
||||
|
||||
def requested?(other_account)
|
||||
follow_requests.where(target_account: other_account).exists?
|
||||
end
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
#
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# id :bigint not null, primary key
|
||||
# target_account_id :bigint not null
|
||||
# account_id :bigint not null
|
||||
# id :bigint not null, primary key
|
||||
# target_account_id :bigint not null
|
||||
# show_reblogs :boolean default(TRUE), not null
|
||||
#
|
||||
|
||||
class Follow < ApplicationRecord
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
#
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# id :bigint not null, primary key
|
||||
# target_account_id :bigint not null
|
||||
# account_id :bigint not null
|
||||
# id :bigint not null, primary key
|
||||
# target_account_id :bigint not null
|
||||
# show_reblogs :boolean default(TRUE), not null
|
||||
#
|
||||
|
||||
class FollowRequest < ApplicationRecord
|
||||
@@ -21,7 +22,7 @@ class FollowRequest < ApplicationRecord
|
||||
validates :account_id, uniqueness: { scope: :target_account_id }
|
||||
|
||||
def authorize!
|
||||
account.follow!(target_account)
|
||||
account.follow!(target_account, reblogs: show_reblogs)
|
||||
MergeWorker.perform_async(target_account.id, account.id)
|
||||
|
||||
destroy!
|
||||
|
||||
7
app/models/glitch.rb
Normal file
7
app/models/glitch.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Glitch
|
||||
def self.table_name_prefix
|
||||
'glitch_'
|
||||
end
|
||||
end
|
||||
66
app/models/glitch/keyword_mute.rb
Normal file
66
app/models/glitch/keyword_mute.rb
Normal file
@@ -0,0 +1,66 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: glitch_keyword_mutes
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# account_id :integer not null
|
||||
# keyword :string not null
|
||||
# whole_word :boolean default(TRUE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class Glitch::KeywordMute < ApplicationRecord
|
||||
belongs_to :account, required: true
|
||||
|
||||
validates_presence_of :keyword
|
||||
|
||||
after_commit :invalidate_cached_matcher
|
||||
|
||||
def self.matcher_for(account_id)
|
||||
Matcher.new(account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def invalidate_cached_matcher
|
||||
Rails.cache.delete("keyword_mutes:regex:#{account_id}")
|
||||
end
|
||||
|
||||
class Matcher
|
||||
attr_reader :account_id
|
||||
attr_reader :regex
|
||||
|
||||
def initialize(account_id)
|
||||
@account_id = account_id
|
||||
regex_text = Rails.cache.fetch("keyword_mutes:regex:#{account_id}") { regex_text_for_account }
|
||||
@regex = /#{regex_text}/
|
||||
end
|
||||
|
||||
def =~(str)
|
||||
regex =~ str
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def keywords
|
||||
Glitch::KeywordMute.where(account_id: account_id).select(:keyword, :id, :whole_word)
|
||||
end
|
||||
|
||||
def regex_text_for_account
|
||||
kws = keywords.find_each.with_object([]) do |kw, a|
|
||||
a << (kw.whole_word ? boundary_regex_for_keyword(kw.keyword) : kw.keyword)
|
||||
end
|
||||
|
||||
Regexp.union(kws).source
|
||||
end
|
||||
|
||||
def boundary_regex_for_keyword(keyword)
|
||||
sb = keyword =~ /\A[[:word:]]/ ? '\b' : ''
|
||||
eb = keyword =~ /[[:word:]]\Z/ ? '\b' : ''
|
||||
|
||||
/(?mix:#{sb}#{Regexp.escape(keyword)}#{eb})/
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -24,15 +24,32 @@ require 'mime/types'
|
||||
class MediaAttachment < ApplicationRecord
|
||||
self.inheritance_column = nil
|
||||
|
||||
enum type: [:image, :gifv, :video, :unknown]
|
||||
enum type: [:image, :gifv, :video, :audio, :unknown]
|
||||
|
||||
IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'].freeze
|
||||
VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v'].freeze
|
||||
AUDIO_FILE_EXTENSIONS = ['.mp3', '.m4a', '.wav', '.ogg'].freeze
|
||||
|
||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
|
||||
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze
|
||||
AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze
|
||||
|
||||
IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze
|
||||
AUDIO_STYLES = {
|
||||
original: {
|
||||
format: 'mp4',
|
||||
convert_options: {
|
||||
output: {
|
||||
filter_complex: '"[0:a]compand,showwaves=s=640x360:mode=line,format=yuv420p[v]"',
|
||||
map: '"[v]" -map 0:a',
|
||||
threads: 2,
|
||||
vcodec: 'libx264',
|
||||
acodec: 'aac',
|
||||
movflags: '+faststart',
|
||||
},
|
||||
},
|
||||
},
|
||||
}.freeze
|
||||
VIDEO_STYLES = {
|
||||
small: {
|
||||
convert_options: {
|
||||
@@ -55,7 +72,7 @@ class MediaAttachment < ApplicationRecord
|
||||
|
||||
include Remotable
|
||||
|
||||
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES
|
||||
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
|
||||
validates_attachment_size :file, less_than: 8.megabytes
|
||||
|
||||
validates :account, presence: true
|
||||
@@ -110,6 +127,8 @@ class MediaAttachment < ApplicationRecord
|
||||
}
|
||||
elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type
|
||||
IMAGE_STYLES
|
||||
elsif AUDIO_MIME_TYPES.include? f.instance.file_content_type
|
||||
AUDIO_STYLES
|
||||
else
|
||||
VIDEO_STYLES
|
||||
end
|
||||
@@ -120,6 +139,8 @@ class MediaAttachment < ApplicationRecord
|
||||
[:gif_transcoder]
|
||||
elsif VIDEO_MIME_TYPES.include? f.file_content_type
|
||||
[:video_transcoder]
|
||||
elsif AUDIO_MIME_TYPES.include? f.file_content_type
|
||||
[:audio_transcoder]
|
||||
else
|
||||
[:thumbnail]
|
||||
end
|
||||
@@ -144,8 +165,8 @@ class MediaAttachment < ApplicationRecord
|
||||
end
|
||||
|
||||
def set_type_and_extension
|
||||
self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image
|
||||
extension = appropriate_extension
|
||||
self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : AUDIO_MIME_TYPES.include?(file_content_type) ? :audio : :image
|
||||
extension = AUDIO_MIME_TYPES.include?(file_content_type) ? '.mp4' : appropriate_extension
|
||||
basename = Paperclip::Interpolations.basename(file, :original)
|
||||
file.instance_write :file_name, [basename, extension].delete_if(&:blank?).join('.')
|
||||
end
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
#
|
||||
# Table name: mutes
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# id :bigint not null, primary key
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer not null
|
||||
# target_account_id :integer not null
|
||||
# account_id :bigint not null
|
||||
# target_account_id :bigint not null
|
||||
# hide_notifications :boolean default(TRUE), not null
|
||||
#
|
||||
|
||||
|
||||
@@ -154,6 +154,14 @@ 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])
|
||||
|
||||
apply_timeline_filters(query, account, false)
|
||||
end
|
||||
|
||||
def as_public_timeline(account = nil, local_only = false)
|
||||
query = timeline_scope(local_only).without_replies
|
||||
|
||||
@@ -261,6 +269,11 @@ class Status < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def local_only?
|
||||
# match both with and without U+FE0F (the emoji variation selector)
|
||||
/👁\ufe0f?\z/.match?(content)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def store_uri
|
||||
|
||||
@@ -27,7 +27,7 @@ class StreamEntry < ApplicationRecord
|
||||
scope :recent, -> { reorder(id: :desc) }
|
||||
scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES) }
|
||||
|
||||
delegate :target, :title, :content, :thread,
|
||||
delegate :target, :title, :content, :thread, :local_only?,
|
||||
to: :status,
|
||||
allow_nil: true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user