Merge tootsuite/master at 3023725936

This commit is contained in:
Surinna Curtis
2017-11-16 01:21:16 -06:00
230 changed files with 8548 additions and 567 deletions

View File

@@ -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?

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
module Glitch
def self.table_name_prefix
'glitch_'
end
end

View 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

View File

@@ -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

View File

@@ -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
#

View File

@@ -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

View File

@@ -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