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.rb6
-rw-r--r--app/models/account_alias.rb2
-rw-r--r--app/models/account_migration.rb2
-rw-r--r--app/models/concerns/account_interactions.rb31
-rw-r--r--app/models/concerns/expireable.rb10
-rw-r--r--app/models/follow.rb8
-rw-r--r--app/models/form/ip_block_batch.rb31
-rw-r--r--app/models/form/redirect.rb2
-rw-r--r--app/models/ip_block.rb41
-rw-r--r--app/models/mute.rb3
-rw-r--r--app/models/remote_follow.rb10
-rw-r--r--app/models/user.rb18
12 files changed, 147 insertions, 17 deletions
diff --git a/app/models/account.rb b/app/models/account.rb
index 228e36755..1b8afe594 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -361,6 +361,12 @@ class Account < ApplicationRecord
     shared_inbox_url.presence || inbox_url
   end
 
+  def synchronization_uri_prefix
+    return 'local' if local?
+
+    @synchronization_uri_prefix ||= uri[/http(s?):\/\/[^\/]+\//]
+  end
+
   def max_visibility_for_domain(domain)
     return 'public' if domain.blank?
 
diff --git a/app/models/account_alias.rb b/app/models/account_alias.rb
index 792e9e8d4..3d659142a 100644
--- a/app/models/account_alias.rb
+++ b/app/models/account_alias.rb
@@ -33,7 +33,7 @@ class AccountAlias < ApplicationRecord
   def set_uri
     target_account = ResolveAccountService.new.call(acct)
     self.uri       = ActivityPub::TagManager.instance.uri_for(target_account) unless target_account.nil?
-  rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
+  rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
     # Validation will take care of it
   end
 
diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb
index 681b5b2cd..4fae98ed7 100644
--- a/app/models/account_migration.rb
+++ b/app/models/account_migration.rb
@@ -54,7 +54,7 @@ class AccountMigration < ApplicationRecord
 
   def set_target_account
     self.target_account = ResolveAccountService.new.call(acct)
-  rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
+  rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
     # Validation will take care of it
   end
 
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index 184064e94..98d06d586 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -132,14 +132,19 @@ module AccountInteractions
                        .find_or_create_by!(target_account: other_account)
   end
 
-  def mute!(other_account, notifications: nil, timelines_only: nil)
+  def mute!(other_account, notifications: nil, timelines_only: nil, duration: 0)
     notifications = true if notifications.nil?
     timelines_only = false if timelines_only.nil?
-    mute = mute_relationships.create_with(hide_notifications: notifications, timelines_only: timelines_only).find_or_create_by!(target_account: other_account)
+    mute = mute_relationships.create_with(hide_notifications: notifications, timelines_only: timelines_only).find_or_initialize_by(target_account: other_account)
+    mute.expires_in = duration.zero? ? nil : duration
+    mute.save!
+
     remove_potential_friendship(other_account)
 
     # When toggling a mute between hiding and allowing notifications, the mute will already exist, so the find_or_create_by! call will return the existing Mute without updating the hide_notifications attribute. Therefore, we check that hide_notifications? is what we want and set it if it isn't.
-    mute.update!(hide_notifications: notifications, timelines_only: timelines_only) if mute.hide_notifications? != notifications
+    if mute.hide_notifications? != notifications || mute.timelines_only? != timelines_only
+      mute.update!(hide_notifications: notifications, timelines_only: timelines_only)
+    end
 
     mute
   end
@@ -253,6 +258,26 @@ module AccountInteractions
          .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
   end
 
+  def remote_followers_hash(url_prefix)
+    Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}") do
+      digest = "\x00" * 32
+      followers.where(Account.arel_table[:uri].matches(url_prefix + '%', false, true)).pluck_each(:uri) do |uri|
+        Xorcist.xor!(digest, Digest::SHA256.digest(uri))
+      end
+      digest.unpack('H*')[0]
+    end
+  end
+
+  def local_followers_hash
+    Rails.cache.fetch("followers_hash:#{id}:local") do
+      digest = "\x00" * 32
+      followers.where(domain: nil).pluck_each(:username) do |username|
+        Xorcist.xor!(digest, Digest::SHA256.digest(ActivityPub::TagManager.instance.uri_for_username(username)))
+      end
+      digest.unpack('H*')[0]
+    end
+  end
+
   private
 
   def remove_potential_friendship(other_account, mutual = false)
diff --git a/app/models/concerns/expireable.rb b/app/models/concerns/expireable.rb
index f7d2bab49..a66a4661b 100644
--- a/app/models/concerns/expireable.rb
+++ b/app/models/concerns/expireable.rb
@@ -6,7 +6,15 @@ module Expireable
   included do
     scope :expired, -> { where.not(expires_at: nil).where('expires_at < ?', Time.now.utc) }
 
-    attr_reader :expires_in
+    def expires_in
+      return @expires_in if defined?(@expires_in)
+
+      if expires_at.nil?
+        nil
+      else
+        (expires_at - created_at).to_i
+      end
+    end
 
     def expires_in=(interval)
       self.expires_at = interval.to_i.seconds.from_now if interval.present?
diff --git a/app/models/follow.rb b/app/models/follow.rb
index 0b4ddbf3f..55a9da792 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -41,8 +41,10 @@ class Follow < ApplicationRecord
 
   before_validation :set_uri, only: :create
   after_create :increment_cache_counters
+  after_create :invalidate_hash_cache
   after_destroy :remove_endorsements
   after_destroy :decrement_cache_counters
+  after_destroy :invalidate_hash_cache
 
   private
 
@@ -63,4 +65,10 @@ class Follow < ApplicationRecord
     account&.decrement_count!(:following_count)
     target_account&.decrement_count!(:followers_count)
   end
+
+  def invalidate_hash_cache
+    return if account.local? && target_account.local?
+
+    Rails.cache.delete("followers_hash:#{target_account_id}:#{account.synchronization_uri_prefix}")
+  end
 end
diff --git a/app/models/form/ip_block_batch.rb b/app/models/form/ip_block_batch.rb
new file mode 100644
index 000000000..f6fe9b593
--- /dev/null
+++ b/app/models/form/ip_block_batch.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class Form::IpBlockBatch
+  include ActiveModel::Model
+  include Authorization
+  include AccountableConcern
+
+  attr_accessor :ip_block_ids, :action, :current_account
+
+  def save
+    case action
+    when 'delete'
+      delete!
+    end
+  end
+
+  private
+
+  def ip_blocks
+    @ip_blocks ||= IpBlock.where(id: ip_block_ids)
+  end
+
+  def delete!
+    ip_blocks.each { |ip_block| authorize(ip_block, :destroy?) }
+
+    ip_blocks.each do |ip_block|
+      ip_block.destroy
+      log_action :destroy, ip_block
+    end
+  end
+end
diff --git a/app/models/form/redirect.rb b/app/models/form/redirect.rb
index a7961f8e8..19ee9faed 100644
--- a/app/models/form/redirect.rb
+++ b/app/models/form/redirect.rb
@@ -32,7 +32,7 @@ class Form::Redirect
 
   def set_target_account
     @target_account = ResolveAccountService.new.call(acct)
-  rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
+  rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
     # Validation will take care of it
   end
 
diff --git a/app/models/ip_block.rb b/app/models/ip_block.rb
new file mode 100644
index 000000000..aedd3ca0d
--- /dev/null
+++ b/app/models/ip_block.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: ip_blocks
+#
+#  id         :bigint(8)        not null, primary key
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#  expires_at :datetime
+#  ip         :inet             default(#<IPAddr: IPv4:0.0.0.0/255.255.255.255>), not null
+#  severity   :integer          default(NULL), not null
+#  comment    :text             default(""), not null
+#
+
+class IpBlock < ApplicationRecord
+  CACHE_KEY = 'blocked_ips'
+
+  include Expireable
+
+  enum severity: {
+    sign_up_requires_approval: 5000,
+    no_access: 9999,
+  }
+
+  validates :ip, :severity, presence: true
+
+  after_commit :reset_cache
+
+  class << self
+    def blocked?(remote_ip)
+      blocked_ips_map = Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(IpBlock.where(severity: :no_access).pluck(:ip)) }
+      blocked_ips_map.include?(remote_ip)
+    end
+  end
+
+  private
+
+  def reset_cache
+    Rails.cache.delete(CACHE_KEY)
+  end
+end
diff --git a/app/models/mute.rb b/app/models/mute.rb
index 11f833d8e..bdd1d27d6 100644
--- a/app/models/mute.rb
+++ b/app/models/mute.rb
@@ -9,12 +9,15 @@
 #  hide_notifications :boolean          default(TRUE), not null
 #  account_id         :bigint(8)        not null
 #  target_account_id  :bigint(8)        not null
+#  hide_notifications :boolean          default(TRUE), not null
+#  expires_at         :datetime
 #  timelines_only     :boolean          default(FALSE), not null
 #
 
 class Mute < ApplicationRecord
   include Paginable
   include RelationshipCacheable
+  include Expireable
 
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb
index 30b84f7d5..911c06713 100644
--- a/app/models/remote_follow.rb
+++ b/app/models/remote_follow.rb
@@ -56,7 +56,7 @@ class RemoteFollow
 
     if domain.nil?
       @addressable_template = Addressable::Template.new("#{authorize_interaction_url}?uri={uri}")
-    elsif redirect_url_link.nil? || redirect_url_link.template.nil?
+    elsif redirect_uri_template.nil?
       missing_resource_error
     else
       @addressable_template = Addressable::Template.new(redirect_uri_template)
@@ -64,16 +64,12 @@ class RemoteFollow
   end
 
   def redirect_uri_template
-    redirect_url_link.template
-  end
-
-  def redirect_url_link
-    acct_resource&.link('http://ostatus.org/schema/1.0/subscribe')
+    acct_resource&.link('http://ostatus.org/schema/1.0/subscribe', 'template')
   end
 
   def acct_resource
     @acct_resource ||= webfinger!("acct:#{acct}")
-  rescue Goldfinger::Error, HTTP::ConnectionError
+  rescue Webfinger::Error, HTTP::ConnectionError
     nil
   end
 
diff --git a/app/models/user.rb b/app/models/user.rb
index afe248f57..4ec5a5d8e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -41,6 +41,7 @@
 #  sign_in_token             :string
 #  sign_in_token_sent_at     :datetime
 #  webauthn_id               :string
+#  sign_up_ip                :inet
 #  username                  :string
 #  kobold                    :string
 #
@@ -99,7 +100,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 :matches_ip, ->(value) { left_joins(:session_activations).where('users.current_sign_in_ip <<= ?', value).or(left_joins(:session_activations).where('users.sign_up_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) }
   scope :lower_username, ->(username) { where('lower(users.username) = lower(?)', username) }
 
@@ -118,7 +119,7 @@ class User < ApplicationRecord
            :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count,
            :expand_spoilers, :default_language, :show_application,
            :advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images,
-           :default_content_type, :system_emoji_font,
+           :disable_swiping, :default_content_type, :system_emoji_font,
            :manual_publish, :style_dashed_nest, :style_underline_a, :style_css_profile,
            :style_css_profile_errors, :style_css_webapp, :style_css_webapp_errors,
            :style_wide_media,
@@ -363,6 +364,7 @@ class User < ApplicationRecord
 
       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 << [created_at, sign_up_ip] if sign_up_ip.present?
 
       arr.sort_by { |pair| pair.first || Time.now.utc }.uniq(&:last).reverse!
     end
@@ -417,7 +419,17 @@ class User < ApplicationRecord
   end
 
   def set_approved
-    self.approved = open_registrations? || valid_invitation? || external?
+    self.approved = begin
+      if sign_up_from_ip_requires_approval?
+        false
+      else
+        open_registrations? || valid_invitation? || external?
+      end
+    end
+  end
+
+  def sign_up_from_ip_requires_approval?
+    !sign_up_ip.nil? && IpBlock.where(severity: :sign_up_requires_approval).where('ip >>= ?', sign_up_ip.to_s).exists?
   end
 
   def open_registrations?