Merge branch 'instance_only_statuses' of https://github.com/masto-donte-com-br/mastodon into hometown-dev

This commit is contained in:
Darius Kazemi
2020-03-02 09:52:41 -08:00
1433 changed files with 34473 additions and 9858 deletions

View File

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

View File

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

View 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

View 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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,13 @@
# frozen_string_literal: true
class CustomEmojiFilter
KEYS = %i(
local
remote
by_domain
shortcode
).freeze
attr_reader :params
def initialize(params)

View File

@ -20,6 +20,7 @@ class CustomFilter < ApplicationRecord
notifications
public
thread
account
).freeze
include Expireable

View File

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

View File

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

View File

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

View File

@ -1,6 +1,11 @@
# frozen_string_literal: true
class InstanceFilter
KEYS = %i(
limited
by_domain
).freeze
attr_reader :params
def initialize(params)

View File

@ -1,6 +1,11 @@
# frozen_string_literal: true
class InviteFilter
KEYS = %i(
available
expired
).freeze
attr_reader :params
def initialize(params)

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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