about summary refs log tree commit diff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/account.rb12
-rw-r--r--app/models/admin.rb7
-rw-r--r--app/models/admin/action_log.rb40
-rw-r--r--app/models/concerns/account_interactions.rb4
-rw-r--r--app/models/form/admin_settings.rb4
-rw-r--r--app/models/form/migration.rb25
-rw-r--r--app/models/form/status_batch.rb8
-rw-r--r--app/models/glitch/keyword_mute.rb72
-rw-r--r--app/models/invite.rb52
-rw-r--r--app/models/invite_filter.rb32
-rw-r--r--app/models/notification.rb10
-rw-r--r--app/models/status.rb3
-rw-r--r--app/models/user.rb22
13 files changed, 264 insertions, 27 deletions
diff --git a/app/models/account.rb b/app/models/account.rb
index a4b8e1c0b..ffd19fa52 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -42,6 +42,7 @@
 #  followers_url           :string           default(""), not null
 #  protocol                :integer          default("ostatus"), not null
 #  memorial                :boolean          default(FALSE), not null
+#  moved_to_account_id     :integer
 #
 
 class Account < ApplicationRecord
@@ -102,6 +103,9 @@ class Account < ApplicationRecord
   has_many :list_accounts, inverse_of: :account, dependent: :destroy
   has_many :lists, through: :list_accounts
 
+  # Account migrations
+  belongs_to :moved_to_account, class_name: 'Account'
+
   scope :remote, -> { where.not(domain: nil) }
   scope :local, -> { where(domain: nil) }
   scope :without_followers, -> { where(followers_count: 0) }
@@ -135,6 +139,10 @@ class Account < ApplicationRecord
     domain.nil?
   end
 
+  def moved?
+    moved_to_account_id.present?
+  end
+
   def acct
     local? ? username : "#{username}@#{domain}"
   end
@@ -208,6 +216,10 @@ class Account < ApplicationRecord
     Rails.cache.fetch("exclude_domains_for:#{id}") { domain_blocks.pluck(:domain) }
   end
 
+  def preferred_inbox_url
+    shared_inbox_url.presence || inbox_url
+  end
+
   class << self
     def readonly_attributes
       super - %w(statuses_count following_count followers_count)
diff --git a/app/models/admin.rb b/app/models/admin.rb
new file mode 100644
index 000000000..d41d18449
--- /dev/null
+++ b/app/models/admin.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Admin
+  def self.table_name_prefix
+    'admin_'
+  end
+end
diff --git a/app/models/admin/action_log.rb b/app/models/admin/action_log.rb
new file mode 100644
index 000000000..4e950fbf7
--- /dev/null
+++ b/app/models/admin/action_log.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: admin_action_logs
+#
+#  id               :integer          not null, primary key
+#  account_id       :integer
+#  action           :string           default(""), not null
+#  target_type      :string
+#  target_id        :integer
+#  recorded_changes :text             default(""), not null
+#  created_at       :datetime         not null
+#  updated_at       :datetime         not null
+#
+
+class Admin::ActionLog < ApplicationRecord
+  serialize :recorded_changes
+
+  belongs_to :account, required: true
+  belongs_to :target, required: true, polymorphic: true
+
+  default_scope -> { order('id desc') }
+
+  def action
+    super.to_sym
+  end
+
+  before_validation :set_changes
+
+  private
+
+  def set_changes
+    case action
+    when :destroy, :create
+      self.recorded_changes = target.attributes
+    when :update, :promote, :demote
+      self.recorded_changes = target.previous_changes
+    end
+  end
+end
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index c41f92581..fdf35a4e3 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -7,7 +7,7 @@ module AccountInteractions
     def following_map(target_account_ids, account_id)
       Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
         mapping[follow.target_account_id] = {
-          reblogs: follow.show_reblogs?
+          reblogs: follow.show_reblogs?,
         }
       end
     end
@@ -31,7 +31,7 @@ module AccountInteractions
     def requested_map(target_account_ids, account_id)
       FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
         mapping[follow_request.target_account_id] = {
-          reblogs: follow_request.show_reblogs?
+          reblogs: follow_request.show_reblogs?,
         }
       end
     end
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index 2b148c82b..c1d2cf420 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -24,8 +24,12 @@ class Form::AdminSettings
     :open_deletion=,
     :timeline_preview,
     :timeline_preview=,
+    :show_staff_badge,
+    :show_staff_badge=,
     :bootstrap_timeline_accounts,
     :bootstrap_timeline_accounts=,
+    :min_invite_role,
+    :min_invite_role=,
     to: Setting
   )
 end
diff --git a/app/models/form/migration.rb b/app/models/form/migration.rb
new file mode 100644
index 000000000..b74987337
--- /dev/null
+++ b/app/models/form/migration.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Form::Migration
+  include ActiveModel::Validations
+
+  attr_accessor :acct, :account
+
+  def initialize(attrs = {})
+    @account = attrs[:account]
+    @acct    = attrs[:account].acct unless @account.nil?
+    @acct    = attrs[:acct].gsub(/\A@/, '').strip unless attrs[:acct].nil?
+  end
+
+  def valid?
+    return false unless super
+    set_account
+    errors.empty?
+  end
+
+  private
+
+  def set_account
+    self.account = (ResolveRemoteAccountService.new.call(acct) if account.nil? && acct.present?)
+  end
+end
diff --git a/app/models/form/status_batch.rb b/app/models/form/status_batch.rb
index a97b4aa28..4f08a3049 100644
--- a/app/models/form/status_batch.rb
+++ b/app/models/form/status_batch.rb
@@ -2,8 +2,9 @@
 
 class Form::StatusBatch
   include ActiveModel::Model
+  include AccountableConcern
 
-  attr_accessor :status_ids, :action
+  attr_accessor :status_ids, :action, :current_account
 
   ACTION_TYPE = %w(nsfw_on nsfw_off delete).freeze
 
@@ -20,11 +21,14 @@ class Form::StatusBatch
 
   def change_sensitive(sensitive)
     media_attached_status_ids = MediaAttachment.where(status_id: status_ids).pluck(:status_id)
+
     ApplicationRecord.transaction do
       Status.where(id: media_attached_status_ids).find_each do |status|
         status.update!(sensitive: sensitive)
+        log_action :update, status
       end
     end
+
     true
   rescue ActiveRecord::RecordInvalid
     false
@@ -33,7 +37,9 @@ class Form::StatusBatch
   def delete_statuses
     Status.where(id: status_ids).find_each do |status|
       RemovalWorker.perform_async(status.id)
+      log_action :destroy, status
     end
+
     true
   end
 end
diff --git a/app/models/glitch/keyword_mute.rb b/app/models/glitch/keyword_mute.rb
index 009de1880..a2481308f 100644
--- a/app/models/glitch/keyword_mute.rb
+++ b/app/models/glitch/keyword_mute.rb
@@ -16,51 +16,85 @@ class Glitch::KeywordMute < ApplicationRecord
 
   validates_presence_of :keyword
 
-  after_commit :invalidate_cached_matcher
+  after_commit :invalidate_cached_matchers
 
-  def self.matcher_for(account_id)
-    Matcher.new(account_id)
+  def self.text_matcher_for(account_id)
+    TextMatcher.new(account_id)
+  end
+
+  def self.tag_matcher_for(account_id)
+    TagMatcher.new(account_id)
   end
 
   private
 
-  def invalidate_cached_matcher
-    Rails.cache.delete("keyword_mutes:regex:#{account_id}")
+  def invalidate_cached_matchers
+    Rails.cache.delete(TextMatcher.cache_key(account_id))
+    Rails.cache.delete(TagMatcher.cache_key(account_id))
   end
 
-  class Matcher
+  class RegexpMatcher
     attr_reader :account_id
     attr_reader :regex
 
     def initialize(account_id)
       @account_id = account_id
-      regex_text = Rails.cache.fetch("keyword_mutes:regex:#{account_id}") { regex_text_for_account }
+      regex_text = Rails.cache.fetch(self.class.cache_key(account_id)) { make_regex_text }
       @regex = /#{regex_text}/
     end
 
-    def =~(str)
-      regex =~ str
+    protected
+
+    def keywords
+      Glitch::KeywordMute.where(account_id: account_id).pluck(:whole_word, :keyword)
     end
 
-    private
+    def boundary_regex_for_keyword(keyword)
+      sb = keyword =~ /\A[[:word:]]/ ? '\b' : ''
+      eb = keyword =~ /[[:word:]]\Z/ ? '\b' : ''
 
-    def keywords
-      Glitch::KeywordMute.where(account_id: account_id).select(:keyword, :id, :whole_word)
+      /(?mix:#{sb}#{Regexp.escape(keyword)}#{eb})/
+    end
+  end
+
+  class TextMatcher < RegexpMatcher
+    def self.cache_key(account_id)
+      format('keyword_mutes:regex:text:%s', account_id)
+    end
+
+    def matches?(str)
+      !!(regex =~ str)
     end
 
-    def regex_text_for_account
-      kws = keywords.find_each.with_object([]) do |kw, a|
-        a << (kw.whole_word ? boundary_regex_for_keyword(kw.keyword) : kw.keyword)
+    private
+
+    def make_regex_text
+      kws = keywords.map! do |whole_word, keyword|
+        whole_word ? boundary_regex_for_keyword(keyword) : keyword
       end
 
       Regexp.union(kws).source
     end
+  end
 
-    def boundary_regex_for_keyword(keyword)
-      sb = keyword =~ /\A[[:word:]]/ ? '\b' : ''
-      eb = keyword =~ /[[:word:]]\Z/ ? '\b' : ''
+  class TagMatcher < RegexpMatcher
+    def self.cache_key(account_id)
+      format('keyword_mutes:regex:tag:%s', account_id)
+    end
 
-      /(?mix:#{sb}#{Regexp.escape(keyword)}#{eb})/
+    def matches?(tags)
+      tags.pluck(:name).any? { |n| regex =~ n }
+    end
+
+    private
+
+    def make_regex_text
+      kws = keywords.map! do |whole_word, keyword|
+        term = (Tag::HASHTAG_RE =~ keyword) ? $1 : keyword
+        whole_word ? boundary_regex_for_keyword(term) : term
+      end
+
+      Regexp.union(kws).source
     end
   end
 end
diff --git a/app/models/invite.rb b/app/models/invite.rb
new file mode 100644
index 000000000..6907c1f1d
--- /dev/null
+++ b/app/models/invite.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: invites
+#
+#  id         :integer          not null, primary key
+#  user_id    :integer
+#  code       :string           default(""), not null
+#  expires_at :datetime
+#  max_uses   :integer
+#  uses       :integer          default(0), not null
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+
+class Invite < ApplicationRecord
+  belongs_to :user, required: true
+  has_many :users, inverse_of: :invite
+
+  scope :available, -> { where(expires_at: nil).or(where('expires_at >= ?', Time.now.utc)) }
+  scope :expired, -> { where.not(expires_at: nil).where('expires_at < ?', Time.now.utc) }
+
+  before_validation :set_code
+
+  attr_reader :expires_in
+
+  def expires_in=(interval)
+    self.expires_at = interval.to_i.seconds.from_now unless interval.blank?
+    @expires_in     = interval
+  end
+
+  def valid_for_use?
+    (max_uses.nil? || uses < max_uses) && !expired?
+  end
+
+  def expire!
+    touch(:expires_at)
+  end
+
+  def expired?
+    !expires_at.nil? && expires_at < Time.now.utc
+  end
+
+  private
+
+  def set_code
+    loop do
+      self.code = ([*('a'..'z'), *('A'..'Z'), *('0'..'9')] - %w(0 1 I l O)).sample(8).join
+      break if Invite.find_by(code: code).nil?
+    end
+  end
+end
diff --git a/app/models/invite_filter.rb b/app/models/invite_filter.rb
new file mode 100644
index 000000000..7d89bad4a
--- /dev/null
+++ b/app/models/invite_filter.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class InviteFilter
+  attr_reader :params
+
+  def initialize(params)
+    @params = params
+  end
+
+  def results
+    scope = Invite.order(created_at: :desc)
+
+    params.each do |key, value|
+      scope.merge!(scope_for(key, value)) if value.present?
+    end
+
+    scope
+  end
+
+  private
+
+  def scope_for(key, _value)
+    case key.to_s
+    when 'available'
+      Invite.available
+    when 'expired'
+      Invite.expired
+    else
+      raise "Unknown filter: #{key}"
+    end
+  end
+end
diff --git a/app/models/notification.rb b/app/models/notification.rb
index a3ffb1f45..976963528 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -24,7 +24,7 @@ class Notification < ApplicationRecord
     favourite:      'Favourite',
   }.freeze
 
-  STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account]].freeze
+  STATUS_INCLUDES = [:account, :application, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :application, :media_attachments, :tags, mentions: :account]].freeze
 
   belongs_to :account
   belongs_to :from_account, class_name: 'Account'
@@ -55,9 +55,11 @@ class Notification < ApplicationRecord
   def target_status
     case type
     when :reblog
-      activity&.reblog
-    when :favourite, :mention
-      activity&.status
+      status&.reblog
+    when :favourite
+      favourite&.status
+    when :mention
+      mention&.status
     end
   end
 
diff --git a/app/models/status.rb b/app/models/status.rb
index 172d3a665..70cfdc1c7 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -183,7 +183,7 @@ class Status < ApplicationRecord
     end
 
     def reblogs_map(status_ids, account_id)
-      select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).map { |s| [s.reblog_of_id, true] }.to_h
+      select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).reorder(nil).map { |s| [s.reblog_of_id, true] }.to_h
     end
 
     def mutes_map(conversation_ids, account_id)
@@ -291,6 +291,7 @@ class Status < ApplicationRecord
 
   def set_visibility
     self.visibility = (account.locked? ? :private : :public) if visibility.nil?
+    self.visibility = reblog.visibility if reblog?
     self.sensitive  = false if sensitive.nil?
   end
 
diff --git a/app/models/user.rb b/app/models/user.rb
index 2458cf298..29bdcbd67 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -33,6 +33,7 @@
 #  account_id                :integer          not null
 #  disabled                  :boolean          default(FALSE), not null
 #  moderator                 :boolean          default(FALSE), not null
+#  invite_id                 :integer
 #
 
 class User < ApplicationRecord
@@ -47,6 +48,7 @@ class User < ApplicationRecord
          otp_number_of_backup_codes: 10
 
   belongs_to :account, inverse_of: :user, required: true
+  belongs_to :invite, counter_cache: :uses
   accepts_nested_attributes_for :account
 
   has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
@@ -77,6 +79,8 @@ class User < ApplicationRecord
            :reduce_motion, :system_font_ui, :noindex, :flavour, :skin,
            to: :settings, prefix: :setting, allow_nil: false
 
+  attr_accessor :invite_code
+
   def confirmed?
     confirmed_at.present?
   end
@@ -95,6 +99,19 @@ class User < ApplicationRecord
     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,
@@ -169,6 +186,11 @@ class User < ApplicationRecord
     session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload
   end
 
+  def invite_code=(code)
+    self.invite  = Invite.find_by(code: code) unless code.blank?
+    @invite_code = code
+  end
+
   protected
 
   def send_devise_notification(notification, *args)