Merge tag 'v2.8.0' into instance_only_statuses
This commit is contained in:
@ -94,7 +94,7 @@ class Account < ApplicationRecord
|
||||
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
|
||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||
scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
|
||||
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)).by_recent_status }
|
||||
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)) }
|
||||
scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
|
||||
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) }
|
||||
scope :popular, -> { order('account_stats.followers_count desc') }
|
||||
@ -104,11 +104,14 @@ class Account < ApplicationRecord
|
||||
:current_sign_in_ip,
|
||||
:current_sign_in_at,
|
||||
:confirmed?,
|
||||
:approved?,
|
||||
:pending?,
|
||||
:admin?,
|
||||
:moderator?,
|
||||
:staff?,
|
||||
:locale,
|
||||
:hides_network?,
|
||||
:shows_application?,
|
||||
to: :user,
|
||||
prefix: true,
|
||||
allow_nil: true
|
||||
@ -240,6 +243,7 @@ class Account < ApplicationRecord
|
||||
def fields_attributes=(attributes)
|
||||
fields = []
|
||||
old_fields = self[:fields] || []
|
||||
old_fields = [] if old_fields.is_a?(Hash)
|
||||
|
||||
if attributes.is_a?(Hash)
|
||||
attributes.each_value do |attr|
|
||||
@ -264,6 +268,7 @@ class Account < ApplicationRecord
|
||||
return if fields.size >= DEFAULT_FIELDS_SIZE
|
||||
|
||||
tmp = self[:fields] || []
|
||||
tmp = [] if tmp.is_a?(Hash)
|
||||
|
||||
(DEFAULT_FIELDS_SIZE - tmp.size).times do
|
||||
tmp << { name: '', value: '' }
|
||||
@ -385,7 +390,7 @@ class Account < ApplicationRecord
|
||||
DeliveryFailureTracker.filter(urls)
|
||||
end
|
||||
|
||||
def search_for(terms, limit = 10)
|
||||
def search_for(terms, limit = 10, offset = 0)
|
||||
textsearch, query = generate_query_for_search(terms)
|
||||
|
||||
sql = <<-SQL.squish
|
||||
@ -397,15 +402,15 @@ class Account < ApplicationRecord
|
||||
AND accounts.suspended = false
|
||||
AND accounts.moved_to_account_id IS NULL
|
||||
ORDER BY rank DESC
|
||||
LIMIT ?
|
||||
LIMIT ? OFFSET ?
|
||||
SQL
|
||||
|
||||
records = find_by_sql([sql, limit])
|
||||
records = find_by_sql([sql, limit, offset])
|
||||
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
||||
records
|
||||
end
|
||||
|
||||
def advanced_search_for(terms, account, limit = 10, following = false)
|
||||
def advanced_search_for(terms, account, limit = 10, following = false, offset = 0)
|
||||
textsearch, query = generate_query_for_search(terms)
|
||||
|
||||
if following
|
||||
@ -426,10 +431,10 @@ class Account < ApplicationRecord
|
||||
AND accounts.moved_to_account_id IS NULL
|
||||
GROUP BY accounts.id
|
||||
ORDER BY rank DESC
|
||||
LIMIT ?
|
||||
LIMIT ? OFFSET ?
|
||||
SQL
|
||||
|
||||
records = find_by_sql([sql, account.id, account.id, account.id, limit])
|
||||
records = find_by_sql([sql, account.id, account.id, account.id, limit, offset])
|
||||
else
|
||||
sql = <<-SQL.squish
|
||||
SELECT
|
||||
@ -442,10 +447,10 @@ class Account < ApplicationRecord
|
||||
AND accounts.moved_to_account_id IS NULL
|
||||
GROUP BY accounts.id
|
||||
ORDER BY rank DESC
|
||||
LIMIT ?
|
||||
LIMIT ? OFFSET ?
|
||||
SQL
|
||||
|
||||
records = find_by_sql([sql, account.id, account.id, limit])
|
||||
records = find_by_sql([sql, account.id, account.id, limit, offset])
|
||||
end
|
||||
|
||||
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
||||
@ -469,6 +474,7 @@ class Account < ApplicationRecord
|
||||
|
||||
before_create :generate_keys
|
||||
before_validation :prepare_contents, if: :local?
|
||||
before_validation :prepare_username, on: :create
|
||||
before_destroy :clean_feed_manager
|
||||
|
||||
private
|
||||
@ -478,6 +484,10 @@ class Account < ApplicationRecord
|
||||
note&.strip!
|
||||
end
|
||||
|
||||
def prepare_username
|
||||
username&.squish!
|
||||
end
|
||||
|
||||
def generate_keys
|
||||
return unless local? && !Rails.env.test?
|
||||
|
||||
|
@ -30,7 +30,8 @@ class AccountConversation < ApplicationRecord
|
||||
if participant_account_ids.empty?
|
||||
[account]
|
||||
else
|
||||
Account.where(id: participant_account_ids)
|
||||
participants = Account.where(id: participant_account_ids)
|
||||
participants.empty? ? [account] : participants
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
class AccountDomainBlock < ApplicationRecord
|
||||
include Paginable
|
||||
include DomainNormalizable
|
||||
|
||||
belongs_to :account
|
||||
validates :domain, presence: true, uniqueness: { scope: :account_id }
|
||||
|
@ -22,7 +22,7 @@ class AccountFilter
|
||||
|
||||
def set_defaults!
|
||||
params['local'] = '1' if params['remote'].blank?
|
||||
params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank?
|
||||
params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank? && params['pending'].blank?
|
||||
end
|
||||
|
||||
def scope_for(key, value)
|
||||
@ -35,6 +35,8 @@ class AccountFilter
|
||||
Account.where(domain: value)
|
||||
when 'active'
|
||||
Account.without_suspended
|
||||
when 'pending'
|
||||
accounts_with_users.merge User.pending
|
||||
when 'silenced'
|
||||
Account.silenced
|
||||
when 'suspended'
|
||||
|
46
app/models/account_identity_proof.rb
Normal file
46
app/models/account_identity_proof.rb
Normal file
@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_identity_proofs
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# provider :string default(""), not null
|
||||
# provider_username :string default(""), not null
|
||||
# token :text default(""), not null
|
||||
# verified :boolean default(FALSE), not null
|
||||
# live :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class AccountIdentityProof < ApplicationRecord
|
||||
belongs_to :account
|
||||
|
||||
validates :provider, inclusion: { in: ProofProvider::SUPPORTED_PROVIDERS }
|
||||
validates :provider_username, format: { with: /\A[a-z0-9_]+\z/i }, length: { minimum: 2, maximum: 30 }
|
||||
validates :provider_username, uniqueness: { scope: [:account_id, :provider] }
|
||||
validates :token, format: { with: /\A[a-f0-9]+\z/ }, length: { maximum: 66 }
|
||||
|
||||
validate :validate_with_provider, if: :token_changed?
|
||||
|
||||
scope :active, -> { where(verified: true, live: true) }
|
||||
|
||||
after_commit :queue_worker, if: :saved_change_to_token?
|
||||
|
||||
delegate :refresh!, :on_success_path, :badge, to: :provider_instance
|
||||
|
||||
def provider_instance
|
||||
@provider_instance ||= ProofProvider.find(provider, self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def queue_worker
|
||||
provider_instance.worker_class.perform_async(id)
|
||||
end
|
||||
|
||||
def validate_with_provider
|
||||
provider_instance.validate!
|
||||
end
|
||||
end
|
@ -7,6 +7,9 @@ module AccountAssociations
|
||||
# Local users
|
||||
has_one :user, inverse_of: :account, dependent: :destroy
|
||||
|
||||
# Identity proofs
|
||||
has_many :identity_proofs, class_name: 'AccountIdentityProof', dependent: :destroy, inverse_of: :account
|
||||
|
||||
# Timelines
|
||||
has_many :stream_entries, inverse_of: :account, dependent: :destroy
|
||||
has_many :statuses, inverse_of: :account, dependent: :destroy
|
||||
@ -26,6 +29,7 @@ module AccountAssociations
|
||||
|
||||
# Media
|
||||
has_many :media_attachments, dependent: :destroy
|
||||
has_many :polls, dependent: :destroy
|
||||
|
||||
# PuSH subscriptions
|
||||
has_many :subscriptions, dependent: :destroy
|
||||
@ -55,5 +59,6 @@ module AccountAssociations
|
||||
|
||||
# Hashtags
|
||||
has_and_belongs_to_many :tags
|
||||
has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
|
||||
end
|
||||
end
|
||||
|
@ -3,7 +3,7 @@
|
||||
module AccountAvatar
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
|
||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
|
||||
LIMIT = 2.megabytes
|
||||
|
||||
class_methods do
|
||||
|
@ -13,7 +13,7 @@ module AccountFinderConcern
|
||||
end
|
||||
|
||||
def representative
|
||||
find_local(Setting.site_contact_username.gsub(/\A@/, '')) || Account.local.find_by(suspended: false)
|
||||
find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')) || Account.local.find_by(suspended: false)
|
||||
end
|
||||
|
||||
def find_local(username)
|
||||
|
@ -3,7 +3,7 @@
|
||||
module AccountHeader
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
|
||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
|
||||
LIMIT = 2.megabytes
|
||||
MAX_PIXELS = 750_000 # 1500x500px
|
||||
|
||||
|
@ -10,6 +10,6 @@ module DomainNormalizable
|
||||
private
|
||||
|
||||
def normalize_domain
|
||||
self.domain = TagManager.instance.normalize_domain(domain)
|
||||
self.domain = TagManager.instance.normalize_domain(domain&.strip)
|
||||
end
|
||||
end
|
||||
|
@ -18,7 +18,11 @@ module Expireable
|
||||
end
|
||||
|
||||
def expired?
|
||||
!expires_at.nil? && expires_at < Time.now.utc
|
||||
expires? && expires_at < Time.now.utc
|
||||
end
|
||||
|
||||
def expires?
|
||||
!expires_at.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
25
app/models/concerns/ldap_authenticable.rb
Normal file
25
app/models/concerns/ldap_authenticable.rb
Normal file
@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module LdapAuthenticable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def ldap_setup(_attributes)
|
||||
self.confirmed_at = Time.now.utc
|
||||
self.admin = false
|
||||
|
||||
save!
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def ldap_get_user(attributes = {})
|
||||
resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first })
|
||||
|
||||
if resource.blank?
|
||||
resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first })
|
||||
resource.ldap_setup(attributes)
|
||||
end
|
||||
|
||||
resource
|
||||
end
|
||||
end
|
||||
end
|
@ -7,6 +7,8 @@ module Omniauthable
|
||||
TEMP_EMAIL_REGEX = /\Achange@me/
|
||||
|
||||
included do
|
||||
devise :omniauthable
|
||||
|
||||
def omniauth_providers
|
||||
Devise.omniauth_configs.keys
|
||||
end
|
||||
|
68
app/models/concerns/pam_authenticable.rb
Normal file
68
app/models/concerns/pam_authenticable.rb
Normal file
@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PamAuthenticable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
devise :pam_authenticatable if ENV['PAM_ENABLED'] == 'true'
|
||||
|
||||
def pam_conflict(_attributes)
|
||||
# Block pam login tries on traditional account
|
||||
end
|
||||
|
||||
def pam_conflict?
|
||||
if Devise.pam_authentication
|
||||
encrypted_password.present? && pam_managed_user?
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def pam_get_name
|
||||
if account.present?
|
||||
account.username
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def pam_setup(_attributes)
|
||||
account = Account.new(username: pam_get_name)
|
||||
account.save!(validate: false)
|
||||
|
||||
self.email = "#{account.username}@#{find_pam_suffix}" if email.nil? && find_pam_suffix
|
||||
self.confirmed_at = Time.now.utc
|
||||
self.admin = false
|
||||
self.account = account
|
||||
|
||||
account.destroy! unless save
|
||||
end
|
||||
|
||||
def self.pam_get_user(attributes = {})
|
||||
return nil unless attributes[:email]
|
||||
|
||||
resource = begin
|
||||
if Devise.check_at_sign && !attributes[:email].index('@')
|
||||
joins(:account).find_by(accounts: { username: attributes[:email] })
|
||||
else
|
||||
find_by(email: attributes[:email])
|
||||
end
|
||||
end
|
||||
|
||||
if resource.nil?
|
||||
resource = new(email: attributes[:email], agreement: true)
|
||||
|
||||
if Devise.check_at_sign && !resource[:email].index('@')
|
||||
resource[:email] = Rpam2.getenv(resource.find_pam_service, attributes[:email], attributes[:password], 'email', false)
|
||||
resource[:email] = "#{attributes[:email]}@#{resource.find_pam_suffix}" unless resource[:email]
|
||||
end
|
||||
end
|
||||
|
||||
resource
|
||||
end
|
||||
|
||||
def self.authenticate_with_pam(attributes = {})
|
||||
super if Devise.pam_authentication
|
||||
end
|
||||
end
|
||||
end
|
@ -11,6 +11,10 @@ module StatusThreadingConcern
|
||||
find_statuses_from_tree_path(descendant_ids(limit, max_child_id, since_child_id, depth), account, promote: true)
|
||||
end
|
||||
|
||||
def self_replies(limit)
|
||||
account.statuses.where(in_reply_to_id: id, visibility: [:public, :unlisted]).reorder(id: :asc).limit(limit)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ancestor_ids(limit)
|
||||
|
54
app/models/concerns/user_roles.rb
Normal file
54
app/models/concerns/user_roles.rb
Normal file
@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module UserRoles
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
scope :admins, -> { where(admin: true) }
|
||||
scope :moderators, -> { where(moderator: true) }
|
||||
scope :staff, -> { admins.or(moderators) }
|
||||
end
|
||||
|
||||
def staff?
|
||||
admin? || moderator?
|
||||
end
|
||||
|
||||
def role
|
||||
if admin?
|
||||
'admin'
|
||||
elsif moderator?
|
||||
'moderator'
|
||||
else
|
||||
'user'
|
||||
end
|
||||
end
|
||||
|
||||
def role?(role)
|
||||
case role
|
||||
when 'user'
|
||||
true
|
||||
when 'moderator'
|
||||
staff?
|
||||
when 'admin'
|
||||
admin?
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def promote!
|
||||
if moderator?
|
||||
update!(moderator: false, admin: true)
|
||||
elsif !admin?
|
||||
update!(moderator: true)
|
||||
end
|
||||
end
|
||||
|
||||
def demote!
|
||||
if admin?
|
||||
update!(admin: false, moderator: true)
|
||||
elsif moderator?
|
||||
update!(moderator: false)
|
||||
end
|
||||
end
|
||||
end
|
@ -24,6 +24,8 @@ class DomainBlock < ApplicationRecord
|
||||
has_many :accounts, foreign_key: :domain, primary_key: :domain
|
||||
delegate :count, to: :accounts, prefix: true
|
||||
|
||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||
|
||||
def self.blocked?(domain)
|
||||
where(domain: domain, severity: :suspend).exists?
|
||||
end
|
||||
|
@ -1,4 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'csv'
|
||||
|
||||
class Export
|
||||
@ -13,16 +14,24 @@ class Export
|
||||
end
|
||||
|
||||
def to_muted_accounts_csv
|
||||
to_csv account.muting.select(:username, :domain)
|
||||
CSV.generate(headers: ['Account address', 'Hide notifications'], write_headers: true) do |csv|
|
||||
account.mute_relationships.includes(:target_account).reorder(id: :desc).each do |mute|
|
||||
csv << [acct(mute.target_account), mute.hide_notifications]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def to_following_accounts_csv
|
||||
to_csv account.following.select(:username, :domain)
|
||||
CSV.generate(headers: ['Account address', 'Show boosts'], write_headers: true) do |csv|
|
||||
account.active_relationships.includes(:target_account).reorder(id: :desc).each do |follow|
|
||||
csv << [acct(follow.target_account), follow.show_reblogs]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def to_lists_csv
|
||||
CSV.generate do |csv|
|
||||
account.owned_lists.select(:title).each do |list|
|
||||
account.owned_lists.select(:title, :id).each do |list|
|
||||
list.accounts.select(:username, :domain).each do |account|
|
||||
csv << [list.title, acct(account)]
|
||||
end
|
||||
|
47
app/models/featured_tag.rb
Normal file
47
app/models/featured_tag.rb
Normal file
@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: featured_tags
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# tag_id :bigint(8)
|
||||
# statuses_count :bigint(8) default(0), not null
|
||||
# last_status_at :datetime
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class FeaturedTag < ApplicationRecord
|
||||
belongs_to :account, inverse_of: :featured_tags, required: true
|
||||
belongs_to :tag, inverse_of: :featured_tags, required: true
|
||||
|
||||
delegate :name, to: :tag, allow_nil: true
|
||||
|
||||
validates_associated :tag, on: :create
|
||||
validates :name, presence: true, on: :create
|
||||
validate :validate_featured_tags_limit, on: :create
|
||||
|
||||
def name=(str)
|
||||
self.tag = Tag.find_or_initialize_by(name: str.strip.delete('#').mb_chars.downcase.to_s)
|
||||
end
|
||||
|
||||
def increment(timestamp)
|
||||
update(statuses_count: statuses_count + 1, last_status_at: timestamp)
|
||||
end
|
||||
|
||||
def decrement(deleted_status_id)
|
||||
update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at)
|
||||
end
|
||||
|
||||
def reset_data
|
||||
self.statuses_count = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).count
|
||||
self.last_status_at = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).select(:created_at).first&.created_at
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_featured_tags_limit
|
||||
errors.add(:base, I18n.t('featured_tags.errors.limit')) if account.featured_tags.count >= 10
|
||||
end
|
||||
end
|
@ -26,7 +26,7 @@ class FollowRequest < ApplicationRecord
|
||||
|
||||
def authorize!
|
||||
account.follow!(target_account, reblogs: show_reblogs, uri: uri)
|
||||
MergeWorker.perform_async(target_account.id, account.id)
|
||||
MergeWorker.perform_async(target_account.id, account.id) if account.local?
|
||||
destroy!
|
||||
end
|
||||
|
||||
|
79
app/models/form/account_batch.rb
Normal file
79
app/models/form/account_batch.rb
Normal file
@ -0,0 +1,79 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Form::AccountBatch
|
||||
include ActiveModel::Model
|
||||
include Authorization
|
||||
|
||||
attr_accessor :account_ids, :action, :current_account
|
||||
|
||||
def save
|
||||
case action
|
||||
when 'unfollow'
|
||||
unfollow!
|
||||
when 'remove_from_followers'
|
||||
remove_from_followers!
|
||||
when 'block_domains'
|
||||
block_domains!
|
||||
when 'approve'
|
||||
approve!
|
||||
when 'reject'
|
||||
reject!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unfollow!
|
||||
accounts.find_each do |target_account|
|
||||
UnfollowService.new.call(current_account, target_account)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_followers!
|
||||
current_account.passive_relationships.where(account_id: account_ids).find_each do |follow|
|
||||
reject_follow!(follow)
|
||||
end
|
||||
end
|
||||
|
||||
def block_domains!
|
||||
AfterAccountDomainBlockWorker.push_bulk(account_domains) do |domain|
|
||||
[current_account.id, domain]
|
||||
end
|
||||
end
|
||||
|
||||
def account_domains
|
||||
accounts.pluck(Arel.sql('distinct domain')).compact
|
||||
end
|
||||
|
||||
def accounts
|
||||
Account.where(id: account_ids)
|
||||
end
|
||||
|
||||
def reject_follow!(follow)
|
||||
follow.destroy
|
||||
|
||||
return unless follow.account.activitypub?
|
||||
|
||||
json = ActiveModelSerializers::SerializableResource.new(
|
||||
follow,
|
||||
serializer: ActivityPub::RejectFollowSerializer,
|
||||
adapter: ActivityPub::Adapter
|
||||
).to_json
|
||||
|
||||
ActivityPub::DeliveryWorker.perform_async(json, current_account.id, follow.account.inbox_url)
|
||||
end
|
||||
|
||||
def approve!
|
||||
users = accounts.includes(:user).map(&:user)
|
||||
|
||||
users.each { |user| authorize(user, :approve?) }
|
||||
.each(&:approve!)
|
||||
end
|
||||
|
||||
def reject!
|
||||
records = accounts.includes(:user)
|
||||
|
||||
records.each { |account| authorize(account.user, :reject?) }
|
||||
.each { |account| SuspendAccountService.new.call(account, including_user: true, destroy: true, skip_distribution: true) }
|
||||
end
|
||||
end
|
@ -3,49 +3,94 @@
|
||||
class Form::AdminSettings
|
||||
include ActiveModel::Model
|
||||
|
||||
delegate(
|
||||
:site_contact_username,
|
||||
:site_contact_username=,
|
||||
:site_contact_email,
|
||||
:site_contact_email=,
|
||||
:site_title,
|
||||
:site_title=,
|
||||
:site_short_description,
|
||||
:site_short_description=,
|
||||
:site_description,
|
||||
:site_description=,
|
||||
:site_extended_description,
|
||||
:site_extended_description=,
|
||||
:site_terms,
|
||||
:site_terms=,
|
||||
:open_registrations,
|
||||
:open_registrations=,
|
||||
:closed_registrations_message,
|
||||
:closed_registrations_message=,
|
||||
:open_deletion,
|
||||
:open_deletion=,
|
||||
:timeline_preview,
|
||||
:timeline_preview=,
|
||||
:show_staff_badge,
|
||||
:show_staff_badge=,
|
||||
:bootstrap_timeline_accounts,
|
||||
:bootstrap_timeline_accounts=,
|
||||
:theme,
|
||||
:theme=,
|
||||
:min_invite_role,
|
||||
:min_invite_role=,
|
||||
:activity_api_enabled,
|
||||
:activity_api_enabled=,
|
||||
:peers_api_enabled,
|
||||
:peers_api_enabled=,
|
||||
:show_known_fediverse_at_about_page,
|
||||
:show_known_fediverse_at_about_page=,
|
||||
:preview_sensitive_media,
|
||||
:preview_sensitive_media=,
|
||||
:custom_css,
|
||||
:custom_css=,
|
||||
:profile_directory,
|
||||
:profile_directory=,
|
||||
to: Setting
|
||||
)
|
||||
KEYS = %i(
|
||||
site_contact_username
|
||||
site_contact_email
|
||||
site_title
|
||||
site_short_description
|
||||
site_description
|
||||
site_extended_description
|
||||
site_terms
|
||||
registrations_mode
|
||||
closed_registrations_message
|
||||
open_deletion
|
||||
timeline_preview
|
||||
show_staff_badge
|
||||
bootstrap_timeline_accounts
|
||||
theme
|
||||
min_invite_role
|
||||
activity_api_enabled
|
||||
peers_api_enabled
|
||||
show_known_fediverse_at_about_page
|
||||
preview_sensitive_media
|
||||
custom_css
|
||||
profile_directory
|
||||
thumbnail
|
||||
hero
|
||||
mascot
|
||||
).freeze
|
||||
|
||||
BOOLEAN_KEYS = %i(
|
||||
open_deletion
|
||||
timeline_preview
|
||||
show_staff_badge
|
||||
activity_api_enabled
|
||||
peers_api_enabled
|
||||
show_known_fediverse_at_about_page
|
||||
preview_sensitive_media
|
||||
profile_directory
|
||||
).freeze
|
||||
|
||||
UPLOAD_KEYS = %i(
|
||||
thumbnail
|
||||
hero
|
||||
mascot
|
||||
).freeze
|
||||
|
||||
attr_accessor(*KEYS)
|
||||
|
||||
validates :site_short_description, :site_description, html: { wrap_with: :p }
|
||||
validates :site_extended_description, :site_terms, :closed_registrations_message, html: true
|
||||
validates :registrations_mode, inclusion: { in: %w(open approved none) }
|
||||
validates :min_invite_role, inclusion: { in: %w(disabled user moderator admin) }
|
||||
validates :site_contact_email, :site_contact_username, presence: true
|
||||
validates :site_contact_username, existing_username: true
|
||||
validates :bootstrap_timeline_accounts, existing_username: { multiple: true }
|
||||
|
||||
def initialize(_attributes = {})
|
||||
super
|
||||
initialize_attributes
|
||||
end
|
||||
|
||||
def save
|
||||
return false unless valid?
|
||||
|
||||
KEYS.each do |key|
|
||||
value = instance_variable_get("@#{key}")
|
||||
|
||||
if UPLOAD_KEYS.include?(key) && !value.nil?
|
||||
upload = SiteUpload.where(var: key).first_or_initialize(var: key)
|
||||
upload.update(file: value)
|
||||
else
|
||||
setting = Setting.where(var: key).first_or_initialize(var: key)
|
||||
setting.update(value: typecast_value(key, value))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize_attributes
|
||||
KEYS.each do |key|
|
||||
instance_variable_set("@#{key}", Setting.public_send(key)) if instance_variable_get("@#{key}").nil?
|
||||
end
|
||||
end
|
||||
|
||||
def typecast_value(key, value)
|
||||
if BOOLEAN_KEYS.include?(key)
|
||||
value == '1'
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -13,20 +13,30 @@
|
||||
# data_file_size :integer
|
||||
# data_updated_at :datetime
|
||||
# account_id :bigint(8) not null
|
||||
# overwrite :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
class Import < ApplicationRecord
|
||||
FILE_TYPES = ['text/plain', 'text/csv'].freeze
|
||||
FILE_TYPES = %w(text/plain text/csv).freeze
|
||||
MODES = %i(merge overwrite).freeze
|
||||
|
||||
self.inheritance_column = false
|
||||
|
||||
belongs_to :account
|
||||
|
||||
enum type: [:following, :blocking, :muting]
|
||||
enum type: [:following, :blocking, :muting, :domain_blocking]
|
||||
|
||||
validates :type, presence: true
|
||||
|
||||
has_attached_file :data
|
||||
validates_attachment_content_type :data, content_type: FILE_TYPES
|
||||
validates_attachment_presence :data
|
||||
|
||||
def mode
|
||||
overwrite? ? :overwrite : :merge
|
||||
end
|
||||
|
||||
def mode=(str)
|
||||
self.overwrite = str.to_sym == :overwrite
|
||||
end
|
||||
end
|
||||
|
@ -7,7 +7,7 @@ class Instance
|
||||
|
||||
def initialize(resource)
|
||||
@domain = resource.domain
|
||||
@accounts_count = resource.accounts_count
|
||||
@accounts_count = resource.is_a?(DomainBlock) ? nil : resource.accounts_count
|
||||
@domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.find_by(domain: domain)
|
||||
end
|
||||
|
||||
@ -15,6 +15,10 @@ class Instance
|
||||
Rails.cache.fetch("#{cache_key}/sample_accounts", expires_in: 12.hours) { Account.where(domain: domain).searchable.joins(:account_stat).popular.limit(3) }
|
||||
end
|
||||
|
||||
def cached_accounts_count
|
||||
@accounts_count || Rails.cache.fetch("#{cache_key}/count", expires_in: 12.hours) { Account.where(domain: domain).count }
|
||||
end
|
||||
|
||||
def to_param
|
||||
domain
|
||||
end
|
||||
|
@ -9,9 +9,13 @@ class InstanceFilter
|
||||
|
||||
def results
|
||||
if params[:limited].present?
|
||||
DomainBlock.order(id: :desc)
|
||||
scope = DomainBlock
|
||||
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
|
||||
scope.order(id: :desc)
|
||||
else
|
||||
Account.remote.by_domain_accounts
|
||||
scope = Account.remote
|
||||
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
|
||||
scope.by_domain_accounts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -25,10 +25,10 @@ class MediaAttachment < ApplicationRecord
|
||||
|
||||
enum type: [:image, :gifv, :video, :unknown]
|
||||
|
||||
IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'].freeze
|
||||
IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].freeze
|
||||
VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v', '.mov'].freeze
|
||||
|
||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
|
||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
|
||||
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime'].freeze
|
||||
VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze
|
||||
|
||||
@ -87,8 +87,8 @@ class MediaAttachment < ApplicationRecord
|
||||
convert_options: { all: '-quality 90 -strip' }
|
||||
|
||||
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES
|
||||
validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video?
|
||||
validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video?
|
||||
validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video_or_gifv?
|
||||
validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video_or_gifv?
|
||||
remotable_attachment :file, VIDEO_LIMIT
|
||||
|
||||
include Attachmentable
|
||||
@ -111,6 +111,10 @@ class MediaAttachment < ApplicationRecord
|
||||
file.blank? && remote_url.present?
|
||||
end
|
||||
|
||||
def video_or_gifv?
|
||||
video? || gifv?
|
||||
end
|
||||
|
||||
def to_param
|
||||
shortcode
|
||||
end
|
||||
|
@ -22,9 +22,10 @@ class Notification < ApplicationRecord
|
||||
follow: 'Follow',
|
||||
follow_request: 'FollowRequest',
|
||||
favourite: 'Favourite',
|
||||
poll: 'Poll',
|
||||
}.freeze
|
||||
|
||||
STATUS_INCLUDES = [:account, :application, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :media_attachments, :tags, active_mentions: :account]].freeze
|
||||
STATUS_INCLUDES = [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account]].freeze
|
||||
|
||||
belongs_to :account, optional: true
|
||||
belongs_to :from_account, class_name: 'Account', optional: true
|
||||
@ -35,6 +36,7 @@ class Notification < ApplicationRecord
|
||||
belongs_to :follow, foreign_type: 'Follow', foreign_key: 'activity_id', optional: true
|
||||
belongs_to :follow_request, foreign_type: 'FollowRequest', foreign_key: 'activity_id', optional: true
|
||||
belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id', optional: true
|
||||
belongs_to :poll, foreign_type: 'Poll', foreign_key: 'activity_id', optional: true
|
||||
|
||||
validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
|
||||
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
|
||||
@ -44,7 +46,7 @@ class Notification < ApplicationRecord
|
||||
where(activity_type: types)
|
||||
}
|
||||
|
||||
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account
|
||||
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, poll: [status: STATUS_INCLUDES]
|
||||
|
||||
def type
|
||||
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
|
||||
@ -58,6 +60,8 @@ class Notification < ApplicationRecord
|
||||
favourite&.status
|
||||
when :mention
|
||||
mention&.status
|
||||
when :poll
|
||||
poll&.status
|
||||
end
|
||||
end
|
||||
|
||||
@ -97,7 +101,7 @@ class Notification < ApplicationRecord
|
||||
return unless new_record?
|
||||
|
||||
case activity_type
|
||||
when 'Status', 'Follow', 'Favourite', 'FollowRequest'
|
||||
when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll'
|
||||
self.from_account_id = activity&.account_id
|
||||
when 'Mention'
|
||||
self.from_account_id = activity&.status&.account_id
|
||||
|
108
app/models/poll.rb
Normal file
108
app/models/poll.rb
Normal file
@ -0,0 +1,108 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: polls
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# status_id :bigint(8)
|
||||
# expires_at :datetime
|
||||
# options :string default([]), not null, is an Array
|
||||
# cached_tallies :bigint(8) default([]), not null, is an Array
|
||||
# multiple :boolean default(FALSE), not null
|
||||
# hide_totals :boolean default(FALSE), not null
|
||||
# votes_count :bigint(8) default(0), not null
|
||||
# last_fetched_at :datetime
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# lock_version :integer default(0), not null
|
||||
#
|
||||
|
||||
class Poll < ApplicationRecord
|
||||
include Expireable
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :status
|
||||
|
||||
has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :destroy
|
||||
|
||||
has_many :notifications, as: :activity, dependent: :destroy
|
||||
|
||||
validates :options, presence: true
|
||||
validates :expires_at, presence: true, if: :local?
|
||||
validates_with PollValidator, on: :create, if: :local?
|
||||
|
||||
scope :attached, -> { where.not(status_id: nil) }
|
||||
scope :unattached, -> { where(status_id: nil) }
|
||||
|
||||
before_validation :prepare_options
|
||||
before_validation :prepare_votes_count
|
||||
|
||||
after_initialize :prepare_cached_tallies
|
||||
|
||||
after_commit :reset_parent_cache, on: :update
|
||||
|
||||
def loaded_options
|
||||
options.map.with_index { |title, key| Option.new(self, key.to_s, title, show_totals_now? ? cached_tallies[key] : nil) }
|
||||
end
|
||||
|
||||
def possibly_stale?
|
||||
remote? && last_fetched_before_expiration? && time_passed_since_last_fetch?
|
||||
end
|
||||
|
||||
def voted?(account)
|
||||
account.id == account_id || votes.where(account: account).exists?
|
||||
end
|
||||
|
||||
delegate :local?, to: :account
|
||||
|
||||
def remote?
|
||||
!local?
|
||||
end
|
||||
|
||||
def emojis
|
||||
@emojis ||= CustomEmoji.from_text(options.join(' '), account.domain)
|
||||
end
|
||||
|
||||
class Option < ActiveModelSerializers::Model
|
||||
attributes :id, :title, :votes_count, :poll
|
||||
|
||||
def initialize(poll, id, title, votes_count)
|
||||
@poll = poll
|
||||
@id = id
|
||||
@title = title
|
||||
@votes_count = votes_count
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prepare_cached_tallies
|
||||
self.cached_tallies = options.map { 0 } if cached_tallies.empty?
|
||||
end
|
||||
|
||||
def prepare_votes_count
|
||||
self.votes_count = cached_tallies.sum unless cached_tallies.empty?
|
||||
end
|
||||
|
||||
def prepare_options
|
||||
self.options = options.map(&:strip).reject(&:blank?)
|
||||
end
|
||||
|
||||
def reset_parent_cache
|
||||
return if status_id.nil?
|
||||
Rails.cache.delete("statuses/#{status_id}")
|
||||
end
|
||||
|
||||
def last_fetched_before_expiration?
|
||||
last_fetched_at.nil? || expires_at.nil? || last_fetched_at < expires_at
|
||||
end
|
||||
|
||||
def time_passed_since_last_fetch?
|
||||
last_fetched_at.nil? || last_fetched_at < 1.minute.ago
|
||||
end
|
||||
|
||||
def show_totals_now?
|
||||
expired? || !hide_totals?
|
||||
end
|
||||
end
|
39
app/models/poll_vote.rb
Normal file
39
app/models/poll_vote.rb
Normal file
@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: poll_votes
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# poll_id :bigint(8)
|
||||
# choice :integer default(0), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# uri :string
|
||||
#
|
||||
|
||||
class PollVote < ApplicationRecord
|
||||
belongs_to :account
|
||||
belongs_to :poll, inverse_of: :votes
|
||||
|
||||
validates :choice, presence: true
|
||||
validates_with VoteValidator
|
||||
|
||||
after_create_commit :increment_counter_cache
|
||||
|
||||
delegate :local?, to: :account
|
||||
|
||||
def object_type
|
||||
:vote
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def increment_counter_cache
|
||||
poll.cached_tallies[choice] = (poll.cached_tallies[choice] || 0) + 1
|
||||
poll.save
|
||||
rescue ActiveRecord::StaleObjectError
|
||||
poll.reload
|
||||
retry
|
||||
end
|
||||
end
|
@ -25,7 +25,7 @@
|
||||
#
|
||||
|
||||
class PreviewCard < ApplicationRecord
|
||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
|
||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
|
||||
LIMIT = 1.megabytes
|
||||
|
||||
self.inheritance_column = false
|
||||
|
@ -13,6 +13,7 @@
|
||||
# action_taken_by_account_id :bigint(8)
|
||||
# target_account_id :bigint(8) not null
|
||||
# assigned_account_id :bigint(8)
|
||||
# uri :string
|
||||
#
|
||||
|
||||
class Report < ApplicationRecord
|
||||
@ -28,6 +29,12 @@ class Report < ApplicationRecord
|
||||
|
||||
validates :comment, length: { maximum: 1000 }
|
||||
|
||||
def local?
|
||||
false # Force uri_for to use uri attribute
|
||||
end
|
||||
|
||||
before_validation :set_uri, only: :create
|
||||
|
||||
def object_type
|
||||
:flag
|
||||
end
|
||||
@ -89,4 +96,8 @@ class Report < ApplicationRecord
|
||||
|
||||
Admin::ActionLog.from("(#{sql}) AS admin_action_logs")
|
||||
end
|
||||
|
||||
def set_uri
|
||||
self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? && account.local?
|
||||
end
|
||||
end
|
||||
|
@ -18,6 +18,7 @@ class SiteUpload < ApplicationRecord
|
||||
has_attached_file :file
|
||||
|
||||
validates_attachment_content_type :file, content_type: /\Aimage\/.*\z/
|
||||
validates :file, presence: true
|
||||
validates :var, presence: true, uniqueness: true
|
||||
|
||||
before_save :set_meta
|
||||
|
@ -21,6 +21,7 @@
|
||||
# account_id :bigint(8) not null
|
||||
# application_id :bigint(8)
|
||||
# in_reply_to_account_id :bigint(8)
|
||||
# poll_id :bigint(8)
|
||||
# local_only :boolean
|
||||
#
|
||||
|
||||
@ -45,6 +46,7 @@ class Status < ApplicationRecord
|
||||
belongs_to :account, inverse_of: :statuses
|
||||
belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account', optional: true
|
||||
belongs_to :conversation, optional: true
|
||||
belongs_to :preloadable_poll, class_name: 'Poll', foreign_key: 'poll_id', optional: true
|
||||
|
||||
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
|
||||
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
|
||||
@ -62,12 +64,16 @@ class Status < ApplicationRecord
|
||||
has_one :notification, as: :activity, dependent: :destroy
|
||||
has_one :stream_entry, as: :activity, inverse_of: :status
|
||||
has_one :status_stat, inverse_of: :status
|
||||
has_one :poll, inverse_of: :status, dependent: :destroy
|
||||
|
||||
validates :uri, uniqueness: true, presence: true, unless: :local?
|
||||
validates :text, presence: true, unless: -> { with_media? || reblog? }
|
||||
validates_with StatusLengthValidator
|
||||
validates_with DisallowedHashtagsValidator
|
||||
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
|
||||
validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
|
||||
|
||||
accepts_nested_attributes_for :poll
|
||||
|
||||
default_scope { recent }
|
||||
|
||||
@ -103,6 +109,7 @@ class Status < ApplicationRecord
|
||||
:tags,
|
||||
:preview_cards,
|
||||
:stream_entry,
|
||||
:preloadable_poll,
|
||||
account: :account_stat,
|
||||
active_mentions: { account: :account_stat },
|
||||
reblog: [
|
||||
@ -113,6 +120,7 @@ class Status < ApplicationRecord
|
||||
:media_attachments,
|
||||
:conversation,
|
||||
:status_stat,
|
||||
:preloadable_poll,
|
||||
account: :account_stat,
|
||||
active_mentions: { account: :account_stat },
|
||||
],
|
||||
@ -211,7 +219,12 @@ class Status < ApplicationRecord
|
||||
end
|
||||
|
||||
def emojis
|
||||
@emojis ||= CustomEmoji.from_text([spoiler_text, text].join(' '), account.domain)
|
||||
return @emojis if defined?(@emojis)
|
||||
|
||||
fields = [spoiler_text, text]
|
||||
fields += preloadable_poll.options unless preloadable_poll.nil?
|
||||
|
||||
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain)
|
||||
end
|
||||
|
||||
def mark_for_mass_destruction!
|
||||
@ -258,6 +271,8 @@ class Status < ApplicationRecord
|
||||
before_validation :set_conversation
|
||||
before_validation :set_local
|
||||
|
||||
after_create :set_poll_id
|
||||
|
||||
class << self
|
||||
def selectable_visibilities
|
||||
visibilities.keys - %w(direct limited)
|
||||
@ -446,9 +461,13 @@ class Status < ApplicationRecord
|
||||
self.reblog = reblog.reblog if reblog? && reblog.reblog?
|
||||
end
|
||||
|
||||
def set_poll_id
|
||||
update_column(:poll_id, poll.id) unless poll.nil?
|
||||
end
|
||||
|
||||
def set_visibility
|
||||
self.visibility = reblog.visibility if reblog? && visibility.nil?
|
||||
self.visibility = (account.locked? ? :private : :public) if visibility.nil?
|
||||
self.visibility = reblog.visibility if reblog?
|
||||
self.sensitive = false if sensitive.nil?
|
||||
end
|
||||
|
||||
|
@ -14,6 +14,7 @@ class Tag < ApplicationRecord
|
||||
has_and_belongs_to_many :accounts
|
||||
has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account'
|
||||
|
||||
has_many :featured_tags, dependent: :destroy, inverse_of: :tag
|
||||
has_one :account_tag_stat, dependent: :destroy
|
||||
|
||||
HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*'
|
||||
@ -23,6 +24,7 @@ class Tag < ApplicationRecord
|
||||
|
||||
scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) }
|
||||
scope :hidden, -> { where(account_tag_stats: { hidden: true }) }
|
||||
scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) }
|
||||
|
||||
delegate :accounts_count,
|
||||
:accounts_count=,
|
||||
@ -62,9 +64,21 @@ class Tag < ApplicationRecord
|
||||
end
|
||||
|
||||
class << self
|
||||
def search_for(term, limit = 5)
|
||||
def search_for(term, limit = 5, offset = 0)
|
||||
pattern = sanitize_sql_like(term.strip) + '%'
|
||||
Tag.where('lower(name) like lower(?)', pattern).order(:name).limit(limit)
|
||||
|
||||
Tag.where('lower(name) like lower(?)', pattern)
|
||||
.order(:name)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
end
|
||||
|
||||
def find_normalized(name)
|
||||
find_by(name: name.mb_chars.downcase.to_s)
|
||||
end
|
||||
|
||||
def find_normalized!(name)
|
||||
find_normalized(name) || raise(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -37,11 +37,12 @@
|
||||
# remember_token :string
|
||||
# chosen_languages :string is an Array
|
||||
# created_by_application_id :bigint(8)
|
||||
# approved :boolean default(TRUE), not null
|
||||
#
|
||||
|
||||
class User < ApplicationRecord
|
||||
include Settings::Extend
|
||||
include Omniauthable
|
||||
include UserRoles
|
||||
|
||||
# The home and list feeds will be stored in Redis for this amount
|
||||
# of time, and status fan-out to followers will include only people
|
||||
@ -61,9 +62,9 @@ class User < ApplicationRecord
|
||||
devise :registerable, :recoverable, :rememberable, :trackable, :validatable,
|
||||
:confirmable
|
||||
|
||||
devise :pam_authenticatable if ENV['PAM_ENABLED'] == 'true'
|
||||
|
||||
devise :omniauthable
|
||||
include Omniauthable
|
||||
include PamAuthenticable
|
||||
include LdapAuthenticable
|
||||
|
||||
belongs_to :account, inverse_of: :user
|
||||
belongs_to :invite, counter_cache: :uses, optional: true
|
||||
@ -73,15 +74,17 @@ class User < ApplicationRecord
|
||||
has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
|
||||
has_many :backups, inverse_of: :user
|
||||
|
||||
has_one :invite_request, class_name: 'UserInviteRequest', inverse_of: :user, dependent: :destroy
|
||||
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? }
|
||||
|
||||
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
|
||||
validates_with BlacklistedEmailValidator, if: :email_changed?
|
||||
validates_with EmailMxValidator, if: :validate_email_dns?
|
||||
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
|
||||
|
||||
scope :recent, -> { order(id: :desc) }
|
||||
scope :admins, -> { where(admin: true) }
|
||||
scope :moderators, -> { where(moderator: true) }
|
||||
scope :staff, -> { admins.or(moderators) }
|
||||
scope :pending, -> { where(approved: false) }
|
||||
scope :approved, -> { where(approved: true) }
|
||||
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
||||
scope :enabled, -> { where(disabled: false) }
|
||||
scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
|
||||
@ -90,6 +93,7 @@ class User < ApplicationRecord
|
||||
scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }
|
||||
|
||||
before_validation :sanitize_languages
|
||||
before_create :set_approved
|
||||
|
||||
# This avoids a deprecation warning from Rails 5.1
|
||||
# It seems possible that a future release of devise-two-factor will
|
||||
@ -100,43 +104,10 @@ 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, :default_federation, to: :settings, prefix: :setting, allow_nil: false
|
||||
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :default_federation, to: :settings, prefix: :setting, allow_nil: false
|
||||
|
||||
attr_reader :invite_code
|
||||
|
||||
def pam_conflict(_)
|
||||
# block pam login tries on traditional account
|
||||
nil
|
||||
end
|
||||
|
||||
def pam_conflict?
|
||||
return false unless Devise.pam_authentication
|
||||
encrypted_password.present? && pam_managed_user?
|
||||
end
|
||||
|
||||
def pam_get_name
|
||||
return account.username if account.present?
|
||||
super
|
||||
end
|
||||
|
||||
def pam_setup(_attributes)
|
||||
acc = Account.new(username: pam_get_name)
|
||||
acc.save!(validate: false)
|
||||
|
||||
self.email = "#{acc.username}@#{find_pam_suffix}" if email.nil? && find_pam_suffix
|
||||
self.confirmed_at = Time.now.utc
|
||||
self.admin = false
|
||||
self.account = acc
|
||||
|
||||
acc.destroy! unless save
|
||||
end
|
||||
|
||||
def ldap_setup(_attributes)
|
||||
self.confirmed_at = Time.now.utc
|
||||
self.admin = false
|
||||
save!
|
||||
end
|
||||
|
||||
def confirmed?
|
||||
confirmed_at.present?
|
||||
end
|
||||
@ -145,33 +116,6 @@ class User < ApplicationRecord
|
||||
invite_id.present?
|
||||
end
|
||||
|
||||
def staff?
|
||||
admin? || moderator?
|
||||
end
|
||||
|
||||
def role
|
||||
if admin?
|
||||
'admin'
|
||||
elsif moderator?
|
||||
'moderator'
|
||||
else
|
||||
'user'
|
||||
end
|
||||
end
|
||||
|
||||
def role?(role)
|
||||
case role
|
||||
when 'user'
|
||||
true
|
||||
when 'moderator'
|
||||
staff?
|
||||
when 'admin'
|
||||
admin?
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def disable!
|
||||
update!(disabled: true,
|
||||
last_sign_in_at: current_sign_in_at,
|
||||
@ -183,18 +127,45 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def confirm
|
||||
new_user = !confirmed?
|
||||
new_user = !confirmed?
|
||||
self.approved = true if open_registrations?
|
||||
|
||||
super
|
||||
prepare_new_user! if new_user
|
||||
|
||||
if new_user && approved?
|
||||
prepare_new_user!
|
||||
elsif new_user
|
||||
notify_staff_about_pending_account!
|
||||
end
|
||||
end
|
||||
|
||||
def confirm!
|
||||
new_user = !confirmed?
|
||||
new_user = !confirmed?
|
||||
self.approved = true if open_registrations?
|
||||
|
||||
skip_confirmation!
|
||||
save!
|
||||
prepare_new_user! if new_user
|
||||
|
||||
prepare_new_user! if new_user && approved?
|
||||
end
|
||||
|
||||
def pending?
|
||||
!approved?
|
||||
end
|
||||
|
||||
def active_for_authentication?
|
||||
super && approved?
|
||||
end
|
||||
|
||||
def inactive_message
|
||||
!approved? ? :pending : super
|
||||
end
|
||||
|
||||
def approve!
|
||||
return if approved?
|
||||
|
||||
update!(approved: true)
|
||||
prepare_new_user!
|
||||
end
|
||||
|
||||
def update_tracked_fields!(request)
|
||||
@ -202,22 +173,6 @@ class User < ApplicationRecord
|
||||
prepare_returning_user!
|
||||
end
|
||||
|
||||
def promote!
|
||||
if moderator?
|
||||
update!(moderator: false, admin: true)
|
||||
elsif !admin?
|
||||
update!(moderator: true)
|
||||
end
|
||||
end
|
||||
|
||||
def demote!
|
||||
if admin?
|
||||
update!(admin: false, moderator: true)
|
||||
elsif moderator?
|
||||
update!(moderator: false)
|
||||
end
|
||||
end
|
||||
|
||||
def disable_two_factor!
|
||||
self.otp_required_for_login = false
|
||||
otp_backup_codes&.clear
|
||||
@ -236,6 +191,10 @@ class User < ApplicationRecord
|
||||
settings.notification_emails['report']
|
||||
end
|
||||
|
||||
def allows_pending_account_emails?
|
||||
settings.notification_emails['pending_account']
|
||||
end
|
||||
|
||||
def hides_network?
|
||||
@hides_network ||= settings.hide_network
|
||||
end
|
||||
@ -244,6 +203,10 @@ class User < ApplicationRecord
|
||||
@aggregates_reblogs ||= settings.aggregate_reblogs
|
||||
end
|
||||
|
||||
def shows_application?
|
||||
@shows_application ||= settings.show_application
|
||||
end
|
||||
|
||||
def token_for_app(a)
|
||||
return nil if a.nil? || a.owner != self
|
||||
Doorkeeper::AccessToken
|
||||
@ -293,43 +256,6 @@ class User < ApplicationRecord
|
||||
super
|
||||
end
|
||||
|
||||
def self.pam_get_user(attributes = {})
|
||||
return nil unless attributes[:email]
|
||||
|
||||
resource =
|
||||
if Devise.check_at_sign && !attributes[:email].index('@')
|
||||
joins(:account).find_by(accounts: { username: attributes[:email] })
|
||||
else
|
||||
find_by(email: attributes[:email])
|
||||
end
|
||||
|
||||
if resource.blank?
|
||||
resource = new(email: attributes[:email], agreement: true)
|
||||
|
||||
if Devise.check_at_sign && !resource[:email].index('@')
|
||||
resource[:email] = Rpam2.getenv(resource.find_pam_service, attributes[:email], attributes[:password], 'email', false)
|
||||
resource[:email] = "#{attributes[:email]}@#{resource.find_pam_suffix}" unless resource[:email]
|
||||
end
|
||||
end
|
||||
resource
|
||||
end
|
||||
|
||||
def self.ldap_get_user(attributes = {})
|
||||
resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first })
|
||||
|
||||
if resource.blank?
|
||||
resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first })
|
||||
resource.ldap_setup(attributes)
|
||||
end
|
||||
|
||||
resource
|
||||
end
|
||||
|
||||
def self.authenticate_with_pam(attributes = {})
|
||||
return nil unless Devise.pam_authentication
|
||||
super
|
||||
end
|
||||
|
||||
def show_all_media?
|
||||
setting_display_media == 'show_all'
|
||||
end
|
||||
@ -346,6 +272,14 @@ class User < ApplicationRecord
|
||||
|
||||
private
|
||||
|
||||
def set_approved
|
||||
self.approved = open_registrations? || invited?
|
||||
end
|
||||
|
||||
def open_registrations?
|
||||
Setting.registrations_mode == 'open'
|
||||
end
|
||||
|
||||
def sanitize_languages
|
||||
return if chosen_languages.nil?
|
||||
chosen_languages.reject!(&:blank?)
|
||||
@ -363,6 +297,13 @@ class User < ApplicationRecord
|
||||
regenerate_feed! if needs_feed_update?
|
||||
end
|
||||
|
||||
def notify_staff_about_pending_account!
|
||||
User.staff.includes(:account).each do |u|
|
||||
next unless u.allows_pending_account_emails?
|
||||
AdminMailer.new_pending_account(u.account, self).deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
def regenerate_feed!
|
||||
return unless Redis.current.setnx("account:#{account_id}:regeneration", true)
|
||||
Redis.current.expire("account:#{account_id}:regeneration", 1.day.seconds)
|
||||
|
17
app/models/user_invite_request.rb
Normal file
17
app/models/user_invite_request.rb
Normal file
@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: user_invite_requests
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# user_id :bigint(8)
|
||||
# text :text
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class UserInviteRequest < ApplicationRecord
|
||||
belongs_to :user, inverse_of: :invite_request
|
||||
validates :text, presence: true, length: { maximum: 420 }
|
||||
end
|
Reference in New Issue
Block a user