Merge branch 'instance_only_statuses' of https://github.com/masto-donte-com-br/mastodon into hometown-dev
This commit is contained in:
@ -50,7 +50,7 @@
|
||||
|
||||
class Account < ApplicationRecord
|
||||
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
|
||||
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
|
||||
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[a-z0-9]+)?)/i
|
||||
|
||||
include AccountAssociations
|
||||
include AccountAvatar
|
||||
@ -70,14 +70,13 @@ class Account < ApplicationRecord
|
||||
enum protocol: [:ostatus, :activitypub]
|
||||
|
||||
validates :username, presence: true
|
||||
validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? }
|
||||
|
||||
# Remote user validations
|
||||
validates :username, uniqueness: { scope: :domain, case_sensitive: true }, if: -> { !local? && will_save_change_to_username? }
|
||||
validates :username, format: { with: /\A#{USERNAME_RE}\z/i }, if: -> { !local? && will_save_change_to_username? }
|
||||
|
||||
# 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 UniqueUsernameValidator, 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, note_length: { maximum: 500 }, if: -> { local? && will_save_change_to_note? }
|
||||
@ -93,6 +92,7 @@ class Account < ApplicationRecord
|
||||
scope :without_silenced, -> { where(silenced_at: nil) }
|
||||
scope :recent, -> { reorder(id: :desc) }
|
||||
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
||||
scope :groups, -> { where(actor_type: 'Group') }
|
||||
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
|
||||
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
|
||||
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
|
||||
@ -153,10 +153,20 @@ class Account < ApplicationRecord
|
||||
self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person'
|
||||
end
|
||||
|
||||
def group?
|
||||
actor_type == 'Group'
|
||||
end
|
||||
|
||||
alias group group?
|
||||
|
||||
def acct
|
||||
local? ? username : "#{username}@#{domain}"
|
||||
end
|
||||
|
||||
def pretty_acct
|
||||
local? ? username : "#{username}@#{Addressable::IDNA.to_unicode(domain)}"
|
||||
end
|
||||
|
||||
def local_username_and_domain
|
||||
"#{username}@#{Rails.configuration.x.local_domain}"
|
||||
end
|
||||
@ -303,10 +313,6 @@ class Account < ApplicationRecord
|
||||
self.fields = tmp
|
||||
end
|
||||
|
||||
def subscription(webhook_url)
|
||||
@subscription ||= OStatus2::Subscription.new(remote_url, secret: secret, webhook: webhook_url, hub: hub_url)
|
||||
end
|
||||
|
||||
def save_with_optional_media!
|
||||
save!
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
@ -429,12 +435,14 @@ class Account < ApplicationRecord
|
||||
SELECT target_account_id
|
||||
FROM follows
|
||||
WHERE account_id = ?
|
||||
UNION ALL
|
||||
SELECT ?
|
||||
)
|
||||
SELECT
|
||||
accounts.*,
|
||||
(count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
|
||||
FROM accounts
|
||||
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
|
||||
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?)
|
||||
WHERE accounts.id IN (SELECT * FROM first_degree)
|
||||
AND #{query} @@ #{textsearch}
|
||||
AND accounts.suspended_at IS NULL
|
||||
@ -467,6 +475,12 @@ class Account < ApplicationRecord
|
||||
records
|
||||
end
|
||||
|
||||
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) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_query_for_search(terms)
|
||||
|
@ -1,6 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AccountFilter
|
||||
KEYS = %i(
|
||||
local
|
||||
remote
|
||||
by_domain
|
||||
active
|
||||
pending
|
||||
silenced
|
||||
suspended
|
||||
username
|
||||
display_name
|
||||
email
|
||||
ip
|
||||
staff
|
||||
).freeze
|
||||
|
||||
attr_reader :params
|
||||
|
||||
def initialize(params)
|
||||
@ -50,7 +65,7 @@ class AccountFilter
|
||||
when 'email'
|
||||
accounts_with_users.merge User.matches_email(value)
|
||||
when 'ip'
|
||||
valid_ip?(value) ? accounts_with_users.where('users.current_sign_in_ip <<= ?', value) : Account.none
|
||||
valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none
|
||||
when 'staff'
|
||||
accounts_with_users.merge User.staff
|
||||
else
|
||||
|
86
app/models/announcement.rb
Normal file
86
app/models/announcement.rb
Normal file
@ -0,0 +1,86 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: announcements
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# text :text default(""), not null
|
||||
# published :boolean default(FALSE), not null
|
||||
# all_day :boolean default(FALSE), not null
|
||||
# scheduled_at :datetime
|
||||
# starts_at :datetime
|
||||
# ends_at :datetime
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# published_at :datetime
|
||||
#
|
||||
|
||||
class Announcement < ApplicationRecord
|
||||
scope :unpublished, -> { where(published: false) }
|
||||
scope :published, -> { where(published: true) }
|
||||
scope :without_muted, ->(account) { joins("LEFT OUTER JOIN announcement_mutes ON announcement_mutes.announcement_id = announcements.id AND announcement_mutes.account_id = #{account.id}").where('announcement_mutes.id IS NULL') }
|
||||
scope :chronological, -> { order(Arel.sql('COALESCE(announcements.starts_at, announcements.scheduled_at, announcements.published_at, announcements.created_at) ASC')) }
|
||||
|
||||
has_many :announcement_mutes, dependent: :destroy
|
||||
has_many :announcement_reactions, dependent: :destroy
|
||||
|
||||
validates :text, presence: true
|
||||
validates :starts_at, presence: true, if: -> { ends_at.present? }
|
||||
validates :ends_at, presence: true, if: -> { starts_at.present? }
|
||||
|
||||
before_validation :set_all_day
|
||||
before_validation :set_published, on: :create
|
||||
|
||||
def publish!
|
||||
update!(published: true, published_at: Time.now.utc, scheduled_at: nil)
|
||||
end
|
||||
|
||||
def unpublish!
|
||||
update!(published: false, scheduled_at: nil)
|
||||
end
|
||||
|
||||
def time_range?
|
||||
starts_at.present? && ends_at.present?
|
||||
end
|
||||
|
||||
def mentions
|
||||
@mentions ||= Account.from_text(text)
|
||||
end
|
||||
|
||||
def tags
|
||||
@tags ||= Tag.find_or_create_by_names(Extractor.extract_hashtags(text))
|
||||
end
|
||||
|
||||
def emojis
|
||||
@emojis ||= CustomEmoji.from_text(text)
|
||||
end
|
||||
|
||||
def reactions(account = nil)
|
||||
records = begin
|
||||
scope = announcement_reactions.group(:announcement_id, :name, :custom_emoji_id).order(Arel.sql('MIN(created_at) ASC'))
|
||||
|
||||
if account.nil?
|
||||
scope.select('name, custom_emoji_id, count(*) as count, false as me')
|
||||
else
|
||||
scope.select("name, custom_emoji_id, count(*) as count, exists(select 1 from announcement_reactions r where r.account_id = #{account.id} and r.announcement_id = announcement_reactions.announcement_id and r.name = announcement_reactions.name) as me")
|
||||
end
|
||||
end
|
||||
|
||||
ActiveRecord::Associations::Preloader.new.preload(records, :custom_emoji)
|
||||
records
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_all_day
|
||||
self.all_day = false if starts_at.blank? || ends_at.blank?
|
||||
end
|
||||
|
||||
def set_published
|
||||
return unless scheduled_at.blank? || scheduled_at.past?
|
||||
|
||||
self.published = true
|
||||
self.published_at = Time.now.utc
|
||||
end
|
||||
end
|
39
app/models/announcement_filter.rb
Normal file
39
app/models/announcement_filter.rb
Normal file
@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AnnouncementFilter
|
||||
KEYS = %i(
|
||||
published
|
||||
unpublished
|
||||
).freeze
|
||||
|
||||
attr_reader :params
|
||||
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def results
|
||||
scope = Announcement.unscoped
|
||||
|
||||
params.each do |key, value|
|
||||
next if key.to_s == 'page'
|
||||
|
||||
scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
|
||||
end
|
||||
|
||||
scope.chronological
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scope_for(key, _value)
|
||||
case key.to_s
|
||||
when 'published'
|
||||
Announcement.published
|
||||
when 'unpublished'
|
||||
Announcement.unpublished
|
||||
else
|
||||
raise "Unknown filter: #{key}"
|
||||
end
|
||||
end
|
||||
end
|
19
app/models/announcement_mute.rb
Normal file
19
app/models/announcement_mute.rb
Normal file
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: announcement_mutes
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# announcement_id :bigint(8)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class AnnouncementMute < ApplicationRecord
|
||||
belongs_to :account
|
||||
belongs_to :announcement, inverse_of: :announcement_mutes
|
||||
|
||||
validates :account_id, uniqueness: { scope: :announcement_id }
|
||||
end
|
37
app/models/announcement_reaction.rb
Normal file
37
app/models/announcement_reaction.rb
Normal file
@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: announcement_reactions
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# announcement_id :bigint(8)
|
||||
# name :string default(""), not null
|
||||
# custom_emoji_id :bigint(8)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class AnnouncementReaction < ApplicationRecord
|
||||
after_commit :queue_publish
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :announcement, inverse_of: :announcement_reactions
|
||||
belongs_to :custom_emoji, optional: true
|
||||
|
||||
validates :name, presence: true
|
||||
validates_with ReactionValidator
|
||||
|
||||
before_validation :set_custom_emoji
|
||||
|
||||
private
|
||||
|
||||
def set_custom_emoji
|
||||
self.custom_emoji = CustomEmoji.local.find_by(disabled: false, shortcode: name) if name.present?
|
||||
end
|
||||
|
||||
def queue_publish
|
||||
PublishAnnouncementReactionWorker.perform_async(announcement_id, name) unless announcement.destroyed?
|
||||
end
|
||||
end
|
@ -7,11 +7,11 @@
|
||||
# user_id :bigint(8)
|
||||
# dump_file_name :string
|
||||
# dump_content_type :string
|
||||
# dump_file_size :integer
|
||||
# dump_updated_at :datetime
|
||||
# processed :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# dump_file_size :bigint(8)
|
||||
#
|
||||
|
||||
class Backup < ApplicationRecord
|
||||
|
26
app/models/bookmark.rb
Normal file
26
app/models/bookmark.rb
Normal file
@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: bookmarks
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8) not null
|
||||
# status_id :bigint(8) not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class Bookmark < ApplicationRecord
|
||||
include Paginable
|
||||
|
||||
update_index('statuses#status', :status) if Chewy.enabled?
|
||||
|
||||
belongs_to :account, inverse_of: :bookmarks
|
||||
belongs_to :status, inverse_of: :bookmarks
|
||||
|
||||
validates :status_id, uniqueness: { scope: :account_id }
|
||||
|
||||
before_validation do
|
||||
self.status = status.reblog if status&.reblog?
|
||||
end
|
||||
end
|
@ -13,6 +13,7 @@ module AccountAssociations
|
||||
# Timelines
|
||||
has_many :statuses, inverse_of: :account, dependent: :destroy
|
||||
has_many :favourites, inverse_of: :account, dependent: :destroy
|
||||
has_many :bookmarks, inverse_of: :account, dependent: :destroy
|
||||
has_many :mentions, inverse_of: :account, dependent: :destroy
|
||||
has_many :notifications, inverse_of: :account, dependent: :destroy
|
||||
has_many :conversations, class_name: 'AccountConversation', dependent: :destroy, inverse_of: :account
|
||||
|
@ -48,7 +48,7 @@ module AccountFinderConcern
|
||||
end
|
||||
|
||||
def with_usernames
|
||||
Account.where.not(username: '')
|
||||
Account.where.not(Account.arel_table[:username].lower.eq '')
|
||||
end
|
||||
|
||||
def matching_username
|
||||
@ -56,11 +56,7 @@ module AccountFinderConcern
|
||||
end
|
||||
|
||||
def matching_domain
|
||||
if domain.nil?
|
||||
Account.where(domain: nil)
|
||||
else
|
||||
Account.where(Account.arel_table[:domain].lower.eq domain.to_s.downcase)
|
||||
end
|
||||
Account.where(Account.arel_table[:domain].lower.eq(domain.nil? ? nil : domain.to_s.downcase))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -84,6 +84,7 @@ module AccountInteractions
|
||||
has_many :muted_by, -> { order('mutes.id desc') }, through: :muted_by_relationships, source: :account
|
||||
has_many :conversation_mutes, dependent: :destroy
|
||||
has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy
|
||||
has_many :announcement_mutes, dependent: :destroy
|
||||
end
|
||||
|
||||
def follow!(other_account, reblogs: nil, uri: nil)
|
||||
@ -186,6 +187,10 @@ module AccountInteractions
|
||||
status.proper.favourites.where(account: self).exists?
|
||||
end
|
||||
|
||||
def bookmarked?(status)
|
||||
status.proper.bookmarks.where(account: self).exists?
|
||||
end
|
||||
|
||||
def reblogged?(status)
|
||||
status.proper.reblogs.where(account: self).exists?
|
||||
end
|
||||
|
@ -9,6 +9,7 @@ module Attachmentable
|
||||
GIF_MATRIX_LIMIT = 921_600 # 1280x720px
|
||||
|
||||
included do
|
||||
before_post_process :obfuscate_file_name
|
||||
before_post_process :set_file_extensions
|
||||
before_post_process :check_image_dimensions
|
||||
before_post_process :set_file_content_type
|
||||
@ -68,4 +69,14 @@ module Attachmentable
|
||||
rescue Terrapin::CommandLineError
|
||||
''
|
||||
end
|
||||
|
||||
def obfuscate_file_name
|
||||
self.class.attachment_definitions.each_key do |attachment_name|
|
||||
attachment = send(attachment_name)
|
||||
|
||||
next if attachment.blank? || attachment.queued_for_write[:original].blank?
|
||||
|
||||
attachment.instance_write :file_name, SecureRandom.hex(8) + File.extname(attachment.instance_read(:file_name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -6,7 +6,7 @@ module LdapAuthenticable
|
||||
class_methods do
|
||||
def authenticate_with_ldap(params = {})
|
||||
ldap = Net::LDAP.new(ldap_options)
|
||||
filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, email: params[:email])
|
||||
filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, mail: Devise.ldap_mail, email: params[:email])
|
||||
|
||||
if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: params[:password]))
|
||||
ldap_get_user(user_info.first)
|
||||
@ -14,10 +14,18 @@ module LdapAuthenticable
|
||||
end
|
||||
|
||||
def ldap_get_user(attributes = {})
|
||||
resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first })
|
||||
safe_username = attributes[Devise.ldap_uid.to_sym].first
|
||||
if Devise.ldap_uid_conversion_enabled
|
||||
keys = Regexp.union(Devise.ldap_uid_conversion_search.chars)
|
||||
replacement = Devise.ldap_uid_conversion_replace
|
||||
|
||||
safe_username = safe_username.gsub(keys, replacement)
|
||||
end
|
||||
|
||||
resource = joins(:account).find_by(accounts: { username: safe_username })
|
||||
|
||||
if resource.blank?
|
||||
resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first }, admin: false, external: true, confirmed_at: Time.now.utc)
|
||||
resource = new(email: attributes[Devise.ldap_mail.to_sym].first, agreement: true, account_attributes: { username: safe_username }, admin: false, external: true, confirmed_at: Time.now.utc)
|
||||
resource.save!
|
||||
end
|
||||
|
||||
|
@ -36,8 +36,8 @@ module Remotable
|
||||
|
||||
basename = SecureRandom.hex(8)
|
||||
|
||||
send("#{attachment_name}=", StringIO.new(response.body_with_limit(limit)))
|
||||
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)
|
||||
end
|
||||
|
@ -81,12 +81,12 @@ module StatusThreadingConcern
|
||||
end
|
||||
|
||||
def find_statuses_from_tree_path(ids, account, promote: false)
|
||||
statuses = statuses_with_accounts(ids).to_a
|
||||
statuses = Status.with_accounts(ids).to_a
|
||||
account_ids = statuses.map(&:account_id).uniq
|
||||
domains = statuses.map(&:account_domain).compact.uniq
|
||||
relations = relations_map_for_account(account, account_ids, domains)
|
||||
|
||||
statuses.reject! { |status| filter_from_context?(status, account, relations) }
|
||||
statuses.reject! { |status| StatusFilter.new(status, account, relations).filtered? }
|
||||
|
||||
# Order ancestors/descendants by tree path
|
||||
statuses.sort_by! { |status| ids.index(status.id) }
|
||||
@ -125,12 +125,4 @@ module StatusThreadingConcern
|
||||
domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id),
|
||||
}
|
||||
end
|
||||
|
||||
def statuses_with_accounts(ids)
|
||||
Status.where(id: ids).includes(:account)
|
||||
end
|
||||
|
||||
def filter_from_context?(status, account, relations)
|
||||
StatusFilter.new(status, account, relations).filtered?
|
||||
end
|
||||
end
|
||||
|
@ -67,7 +67,7 @@ class CustomEmoji < ApplicationRecord
|
||||
end
|
||||
|
||||
class << self
|
||||
def from_text(text, domain)
|
||||
def from_text(text, domain = nil)
|
||||
return [] if text.blank?
|
||||
|
||||
shortcodes = text.scan(SCAN_RE).map(&:first).uniq
|
||||
|
@ -1,6 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CustomEmojiFilter
|
||||
KEYS = %i(
|
||||
local
|
||||
remote
|
||||
by_domain
|
||||
shortcode
|
||||
).freeze
|
||||
|
||||
attr_reader :params
|
||||
|
||||
def initialize(params)
|
||||
|
@ -20,6 +20,7 @@ class CustomFilter < ApplicationRecord
|
||||
notifications
|
||||
public
|
||||
thread
|
||||
account
|
||||
).freeze
|
||||
|
||||
include Expireable
|
||||
|
@ -54,7 +54,7 @@ class DomainBlock < ApplicationRecord
|
||||
segments = uri.normalized_host.split('.')
|
||||
variants = segments.map.with_index { |_, i| segments[i..-1].join('.') }
|
||||
|
||||
where(domain: variants[0..-2]).order(Arel.sql('char_length(domain) desc')).first
|
||||
where(domain: variants).order(Arel.sql('char_length(domain) desc')).first
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -16,6 +16,7 @@ class Form::AdminSettings
|
||||
open_deletion
|
||||
timeline_preview
|
||||
show_staff_badge
|
||||
enable_bootstrap_timeline_accounts
|
||||
bootstrap_timeline_accounts
|
||||
theme
|
||||
min_invite_role
|
||||
@ -40,6 +41,7 @@ class Form::AdminSettings
|
||||
open_deletion
|
||||
timeline_preview
|
||||
show_staff_badge
|
||||
enable_bootstrap_timeline_accounts
|
||||
activity_api_enabled
|
||||
peers_api_enabled
|
||||
show_known_fediverse_at_about_page
|
||||
|
@ -40,7 +40,7 @@ class Form::CustomEmojiBatch
|
||||
if category_id.present?
|
||||
CustomEmojiCategory.find(category_id)
|
||||
elsif category_name.present?
|
||||
CustomEmojiCategory.create!(name: category_name)
|
||||
CustomEmojiCategory.find_or_create_by!(name: category_name)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class InstanceFilter
|
||||
KEYS = %i(
|
||||
limited
|
||||
by_domain
|
||||
).freeze
|
||||
|
||||
attr_reader :params
|
||||
|
||||
def initialize(params)
|
||||
|
@ -1,6 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class InviteFilter
|
||||
KEYS = %i(
|
||||
available
|
||||
expired
|
||||
).freeze
|
||||
|
||||
attr_reader :params
|
||||
|
||||
def initialize(params)
|
||||
|
@ -6,13 +6,13 @@
|
||||
# id :bigint(8) not null, primary key
|
||||
# list_id :bigint(8) not null
|
||||
# account_id :bigint(8) not null
|
||||
# follow_id :bigint(8) not null
|
||||
# follow_id :bigint(8)
|
||||
#
|
||||
|
||||
class ListAccount < ApplicationRecord
|
||||
belongs_to :list
|
||||
belongs_to :account
|
||||
belongs_to :follow
|
||||
belongs_to :follow, optional: true
|
||||
|
||||
validates :account_id, uniqueness: { scope: :list_id }
|
||||
|
||||
@ -21,6 +21,6 @@ class ListAccount < ApplicationRecord
|
||||
private
|
||||
|
||||
def set_follow
|
||||
self.follow = Follow.find_by(account_id: list.account_id, target_account_id: account.id)
|
||||
self.follow = Follow.find_by!(account_id: list.account_id, target_account_id: account.id) unless list.account_id == account.id
|
||||
end
|
||||
end
|
||||
|
@ -26,6 +26,8 @@ class MediaAttachment < ApplicationRecord
|
||||
|
||||
enum type: [:image, :gifv, :video, :unknown, :audio]
|
||||
|
||||
MAX_DESCRIPTION_LENGTH = 1_500
|
||||
|
||||
IMAGE_FILE_EXTENSIONS = %w(.jpg .jpeg .png .gif).freeze
|
||||
VIDEO_FILE_EXTENSIONS = %w(.webm .mp4 .m4v .mov).freeze
|
||||
AUDIO_FILE_EXTENSIONS = %w(.ogg .oga .mp3 .wav .flac .opus .aac .m4a .3gp .wma).freeze
|
||||
@ -87,6 +89,7 @@ class MediaAttachment < ApplicationRecord
|
||||
convert_options: {
|
||||
output: {
|
||||
'loglevel' => 'fatal',
|
||||
'map_metadata' => '-1',
|
||||
'q:a' => 2,
|
||||
},
|
||||
},
|
||||
@ -138,7 +141,8 @@ class MediaAttachment < ApplicationRecord
|
||||
include Attachmentable
|
||||
|
||||
validates :account, presence: true
|
||||
validates :description, length: { maximum: 1_500 }, if: :local?
|
||||
validates :description, length: { maximum: MAX_DESCRIPTION_LENGTH }, if: :local?
|
||||
validates :file, presence: true, if: :local?
|
||||
|
||||
scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) }
|
||||
scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) }
|
||||
@ -164,6 +168,18 @@ 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
|
||||
@ -187,9 +203,12 @@ class MediaAttachment < ApplicationRecord
|
||||
end
|
||||
|
||||
after_commit :reset_parent_cache, on: :update
|
||||
|
||||
before_create :prepare_description, unless: :local?
|
||||
before_create :set_shortcode
|
||||
|
||||
before_post_process :set_type_and_extension
|
||||
|
||||
before_save :set_meta
|
||||
|
||||
class << self
|
||||
@ -242,7 +261,7 @@ class MediaAttachment < ApplicationRecord
|
||||
end
|
||||
|
||||
def prepare_description
|
||||
self.description = description.strip[0...420] unless description.nil?
|
||||
self.description = description.strip[0...MAX_DESCRIPTION_LENGTH] unless description.nil?
|
||||
end
|
||||
|
||||
def set_type_and_extension
|
||||
@ -284,7 +303,7 @@ class MediaAttachment < ApplicationRecord
|
||||
width: width,
|
||||
height: height,
|
||||
size: "#{width}x#{height}",
|
||||
aspect: width.to_f / height.to_f,
|
||||
aspect: width.to_f / height,
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -42,7 +42,7 @@ class Notification < ApplicationRecord
|
||||
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
|
||||
|
||||
scope :browserable, ->(exclude_types = [], account_id = nil) {
|
||||
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types + [:follow_request])
|
||||
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types)
|
||||
if account_id.nil?
|
||||
where(activity_type: types)
|
||||
else
|
||||
@ -50,7 +50,7 @@ class Notification < ApplicationRecord
|
||||
end
|
||||
}
|
||||
|
||||
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, poll: [status: STATUS_INCLUDES]
|
||||
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, follow_request: :account, poll: [status: STATUS_INCLUDES]
|
||||
|
||||
def type
|
||||
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
|
||||
@ -69,10 +69,6 @@ class Notification < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def browserable?
|
||||
type != :follow_request
|
||||
end
|
||||
|
||||
class << self
|
||||
def cache_ids
|
||||
select(:id, :updated_at, :activity_type, :activity_id)
|
||||
|
@ -36,7 +36,7 @@ class Poll < ApplicationRecord
|
||||
scope :attached, -> { where.not(status_id: nil) }
|
||||
scope :unattached, -> { where(status_id: nil) }
|
||||
|
||||
before_validation :prepare_options
|
||||
before_validation :prepare_options, if: :local?
|
||||
before_validation :prepare_votes_count
|
||||
|
||||
after_initialize :prepare_cached_tallies
|
||||
|
120
app/models/relationship_filter.rb
Normal file
120
app/models/relationship_filter.rb
Normal file
@ -0,0 +1,120 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RelationshipFilter
|
||||
KEYS = %i(
|
||||
relationship
|
||||
status
|
||||
by_domain
|
||||
activity
|
||||
order
|
||||
location
|
||||
).freeze
|
||||
|
||||
attr_reader :params, :account
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
@params = params
|
||||
|
||||
set_defaults!
|
||||
end
|
||||
|
||||
def results
|
||||
scope = scope_for('relationship', params['relationship'].to_s.strip)
|
||||
|
||||
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 set_defaults!
|
||||
params['relationship'] = 'following' if params['relationship'].blank?
|
||||
params['order'] = 'recent' if params['order'].blank?
|
||||
end
|
||||
|
||||
def scope_for(key, value)
|
||||
case key
|
||||
when 'relationship'
|
||||
relationship_scope(value)
|
||||
when 'by_domain'
|
||||
by_domain_scope(value)
|
||||
when 'location'
|
||||
location_scope(value)
|
||||
when 'status'
|
||||
status_scope(value)
|
||||
when 'order'
|
||||
order_scope(value)
|
||||
when 'activity'
|
||||
activity_scope(value)
|
||||
else
|
||||
raise "Unknown filter: #{key}"
|
||||
end
|
||||
end
|
||||
|
||||
def relationship_scope(value)
|
||||
case value
|
||||
when 'following'
|
||||
account.following.eager_load(:account_stat).reorder(nil)
|
||||
when 'followed_by'
|
||||
account.followers.eager_load(:account_stat).reorder(nil)
|
||||
when 'mutual'
|
||||
account.followers.eager_load(:account_stat).reorder(nil).merge(Account.where(id: account.following))
|
||||
when 'invited'
|
||||
Account.joins(user: :invite).merge(Invite.where(user: account.user)).eager_load(:account_stat).reorder(nil)
|
||||
else
|
||||
raise "Unknown relationship: #{value}"
|
||||
end
|
||||
end
|
||||
|
||||
def by_domain_scope(value)
|
||||
Account.where(domain: value)
|
||||
end
|
||||
|
||||
def location_scope(value)
|
||||
case value
|
||||
when 'local'
|
||||
Account.local
|
||||
when 'remote'
|
||||
Account.remote
|
||||
else
|
||||
raise "Unknown location: #{value}"
|
||||
end
|
||||
end
|
||||
|
||||
def status_scope(value)
|
||||
case value
|
||||
when 'moved'
|
||||
Account.where.not(moved_to_account_id: nil)
|
||||
when 'primary'
|
||||
Account.where(moved_to_account_id: nil)
|
||||
else
|
||||
raise "Unknown status: #{value}"
|
||||
end
|
||||
end
|
||||
|
||||
def order_scope(value)
|
||||
case value
|
||||
when 'active'
|
||||
Account.by_recent_status
|
||||
when 'recent'
|
||||
params[:relationship] == 'invited' ? Account.recent : Follow.recent
|
||||
else
|
||||
raise "Unknown order: #{value}"
|
||||
end
|
||||
end
|
||||
|
||||
def activity_scope(value)
|
||||
case value
|
||||
when 'dormant'
|
||||
AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(1.month.ago)))
|
||||
else
|
||||
raise "Unknown activity: #{value}"
|
||||
end
|
||||
end
|
||||
end
|
@ -1,6 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ReportFilter
|
||||
KEYS = %i(
|
||||
resolved
|
||||
account_id
|
||||
target_account_id
|
||||
by_target_domain
|
||||
).freeze
|
||||
|
||||
attr_reader :params
|
||||
|
||||
def initialize(params)
|
||||
@ -19,6 +26,8 @@ class ReportFilter
|
||||
|
||||
def scope_for(key, value)
|
||||
case key.to_sym
|
||||
when :by_target_domain
|
||||
Report.where(target_account: Account.where(domain: value))
|
||||
when :resolved
|
||||
Report.resolved
|
||||
when :account_id
|
||||
|
@ -22,9 +22,9 @@
|
||||
# application_id :bigint(8)
|
||||
# in_reply_to_account_id :bigint(8)
|
||||
# poll_id :bigint(8)
|
||||
# deleted_at :datetime
|
||||
# local_only :boolean
|
||||
# activity_pub_type :string
|
||||
# deleted_at :datetime
|
||||
#
|
||||
|
||||
class Status < ApplicationRecord
|
||||
@ -56,6 +56,7 @@ class Status < ApplicationRecord
|
||||
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
|
||||
|
||||
has_many :favourites, inverse_of: :status, dependent: :destroy
|
||||
has_many :bookmarks, inverse_of: :status, dependent: :destroy
|
||||
has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy
|
||||
has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread
|
||||
has_many :mentions, dependent: :destroy, inverse_of: :status
|
||||
@ -84,6 +85,7 @@ class Status < ApplicationRecord
|
||||
scope :remote, -> { where(local: false).where.not(uri: nil) }
|
||||
scope :local, -> { where(local: true).or(where(uri: nil)) }
|
||||
|
||||
scope :with_accounts, ->(ids) { where(id: ids).includes(:account) }
|
||||
scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
|
||||
scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
|
||||
scope :without_local_only, -> { where(local_only: [false, nil]) }
|
||||
@ -200,8 +202,12 @@ class Status < ApplicationRecord
|
||||
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
|
||||
reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}"
|
||||
preview = sensitive ? '<sensitive>' : text.slice(0, 20).split("\n")[0]
|
||||
"#{account.acct}: #{preview}"
|
||||
end
|
||||
end
|
||||
|
||||
@ -311,6 +317,10 @@ class Status < ApplicationRecord
|
||||
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
|
||||
|
||||
def bookmarks_map(status_ids, account_id)
|
||||
Bookmark.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |f| [f.status_id, true] }.to_h
|
||||
end
|
||||
|
||||
def reblogs_map(status_ids, account_id)
|
||||
unscoped.select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).each_with_object({}) { |s, h| h[s.reblog_of_id] = true }
|
||||
end
|
||||
|
@ -117,7 +117,7 @@ class Tag < ApplicationRecord
|
||||
class << self
|
||||
def find_or_create_by_names(name_or_names)
|
||||
Array(name_or_names).map(&method(:normalize)).uniq { |str| str.mb_chars.downcase.to_s }.map do |normalized_name|
|
||||
tag = matching_name(normalized_name).first || create!(name: normalized_name)
|
||||
tag = matching_name(normalized_name).first || create(name: normalized_name)
|
||||
|
||||
yield tag if block_given?
|
||||
|
||||
|
@ -1,6 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TagFilter
|
||||
KEYS = %i(
|
||||
directory
|
||||
reviewed
|
||||
unreviewed
|
||||
pending_review
|
||||
popular
|
||||
active
|
||||
name
|
||||
).freeze
|
||||
|
||||
attr_reader :params
|
||||
|
||||
def initialize(params)
|
||||
|
@ -93,6 +93,7 @@ class User < ApplicationRecord
|
||||
scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
|
||||
scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) }
|
||||
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
|
||||
scope :matches_ip, ->(value) { left_joins(:session_activations).where('users.current_sign_in_ip <<= ?', value).or(left_joins(:session_activations).where('users.last_sign_in_ip <<= ?', value)).or(left_joins(:session_activations).where('session_activations.ip <<= ?', value)) }
|
||||
scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }
|
||||
|
||||
before_validation :sanitize_languages
|
||||
@ -108,7 +109,7 @@ class User < ApplicationRecord
|
||||
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
|
||||
:reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network,
|
||||
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
|
||||
:advanced_layout, :default_federation, :use_blurhash, :use_pending_items, :trends,
|
||||
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images, :default_federation,
|
||||
to: :settings, prefix: :setting, allow_nil: false
|
||||
|
||||
attr_reader :invite_code
|
||||
@ -127,9 +128,7 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def disable!
|
||||
update!(disabled: true,
|
||||
last_sign_in_at: current_sign_in_at,
|
||||
current_sign_in_at: nil)
|
||||
update!(disabled: true)
|
||||
end
|
||||
|
||||
def enable!
|
||||
@ -246,7 +245,7 @@ class User < ApplicationRecord
|
||||
ip: request.remote_ip).session_id
|
||||
end
|
||||
|
||||
def exclusive_session(id)
|
||||
def clear_other_sessions(id)
|
||||
session_activations.exclusive(id)
|
||||
end
|
||||
|
||||
@ -289,6 +288,21 @@ class User < ApplicationRecord
|
||||
setting_display_media == 'hide_all'
|
||||
end
|
||||
|
||||
def recent_ips
|
||||
@recent_ips ||= begin
|
||||
arr = []
|
||||
|
||||
session_activations.each do |session_activation|
|
||||
arr << [session_activation.updated_at, session_activation.ip]
|
||||
end
|
||||
|
||||
arr << [current_sign_in_at, current_sign_in_ip] if current_sign_in_ip.present?
|
||||
arr << [last_sign_in_at, last_sign_in_ip] if last_sign_in_ip.present?
|
||||
|
||||
arr.sort_by { |pair| pair.first || Time.now.utc }.uniq(&:last).reverse!
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def send_devise_notification(notification, *args)
|
||||
|
Reference in New Issue
Block a user