diff --git a/app/lib/vacuum/invites_vacuum.rb b/app/lib/vacuum/invites_vacuum.rb new file mode 100644 index 0000000000..3d7baba7d7 --- /dev/null +++ b/app/lib/vacuum/invites_vacuum.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Vacuum::InvitesVacuum + def initialize(retention_period, max_uses) + @retention_period = retention_period + @max_uses = max_uses + end + + def perform + expire_invites! if @retention_period.present? + end + + private + + def expire_invites! + invites = Invite.available.where('created_at < ?', @retention_period.ago) + invites = if @max_uses.present? + invites.where('max_uses > ? OR max_uses IS NULL', @max_uses) + else + invites.where(max_uses: nil) + end + + invites.reorder(nil).find_each(&:expire!) + end +end diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index cb37a52217..c0b244ffe0 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -34,6 +34,8 @@ class Form::AdminSettings media_cache_retention_period content_cache_retention_period backups_retention_period + invite_expiration_period + invite_max_uses status_page_url captcha_enabled authorized_fetch @@ -43,6 +45,8 @@ class Form::AdminSettings media_cache_retention_period content_cache_retention_period backups_retention_period + invite_expiration_period + invite_max_uses ).freeze BOOLEAN_KEYS = %i( @@ -78,6 +82,7 @@ class Form::AdminSettings validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) } validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) } validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) } + validates :invite_expiration_period, :invite_max_uses, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@invite_expiration_period) || defined?(@invite_max_uses) } validates :site_short_description, length: { maximum: 200 }, if: -> { defined?(@site_short_description) } validates :status_page_url, url: true, allow_blank: true validate :validate_site_uploads diff --git a/app/models/invite_retention_policy.rb b/app/models/invite_retention_policy.rb new file mode 100644 index 0000000000..ff06f0e26b --- /dev/null +++ b/app/models/invite_retention_policy.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class InviteRetentionPolicy + def self.current + new + end + + def invite_retention_period + retention_period Setting.invite_retention_period + end + + def invite_max_uses + max_uses Setting.invite_max_uses + end + + private + + def retention_period(value) + value.days if value.is_a?(Integer) && value.positive? + end + + def max_uses(value) + value if value.is_a?(Integer) && value.positive? + end +end diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml index 4dbc5fbecf..9bfaa1ff3f 100644 --- a/app/views/admin/settings/registrations/show.html.haml +++ b/app/views/admin/settings/registrations/show.html.haml @@ -27,6 +27,10 @@ disabled: !approved_registrations?, wrapper: :with_label + .fields-group + = f.input :invite_expiration_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' } + = f.input :invite_max_uses, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' } + - if captcha_available? .fields-group = f.input :captcha_enabled, diff --git a/app/workers/scheduler/vacuum_scheduler.rb b/app/workers/scheduler/vacuum_scheduler.rb index 1c9a2aabe3..cb1ce74f09 100644 --- a/app/workers/scheduler/vacuum_scheduler.rb +++ b/app/workers/scheduler/vacuum_scheduler.rb @@ -25,6 +25,7 @@ class Scheduler::VacuumScheduler applications_vacuum, feeds_vacuum, imports_vacuum, + invites_vacuum, ] end @@ -60,7 +61,18 @@ class Scheduler::VacuumScheduler Vacuum::ApplicationsVacuum.new end + def invites_vacuum + Vacuum::InvitesVacuum.new( + invites_retention_policy.invite_retention_period, + invites_retention_policy.invite_max_uses + ) + end + def content_retention_policy ContentRetentionPolicy.current end + + def invites_retention_policy + InviteRetentionPolicy.current + end end diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 7304bdc22f..3e225a7c1c 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -82,6 +82,8 @@ en: closed_registrations_message: Displayed when sign-ups are closed content_cache_retention_period: All posts from other servers (including boosts and replies) will be deleted after the specified number of days, without regard to any local user interaction with those posts. This includes posts where a local user has marked it as bookmarks or favorites. Private mentions between users from different instances will also be lost and impossible to restore. Use of this setting is intended for special purpose instances and breaks many user expectations when implemented for general purpose use. custom_css: You can apply custom styles on the web version of Mastodon. + invite_expiration_period: If set to a positive number, invitation links will expire after the specified number of days. + invite_max_uses: If set, invite links with fewer than the specified number of uses will not be forced to expire. mascot: Overrides the illustration in the advanced web interface. media_cache_retention_period: Media files from posts made by remote users are cached on your server. When set to a positive value, media will be deleted after the specified number of days. If the media data is requested after it is deleted, it will be re-downloaded, if the source content is still available. Due to restrictions on how often link preview cards poll third-party sites, it is recommended to set this value to at least 14 days, or link preview cards will not be updated on demand before that time. peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense. @@ -245,6 +247,8 @@ en: closed_registrations_message: Custom message when sign-ups are not available content_cache_retention_period: Remote content retention period custom_css: Custom CSS + invite_expiration_period: Invitation link force expiration period + invite_max_uses: Max uses threshold for force expiration of invitation link mascot: Custom mascot (legacy) media_cache_retention_period: Media cache retention period peers_api_enabled: Publish list of discovered servers in the API diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml index 6ab4e03222..1e9df2f892 100644 --- a/config/locales/simple_form.ko.yml +++ b/config/locales/simple_form.ko.yml @@ -80,6 +80,8 @@ ko: bootstrap_timeline_accounts: 이 계정들은 팔로우 추천 목록 상단에 고정됩니다. closed_registrations_message: 새 가입을 차단했을 때 표시됩니다 custom_css: 사용자 지정 스타일을 웹 버전의 마스토돈에 지정할 수 있습니다. + invite_expiration_period: 양수로 설정된 경우 지정된 일수가 지난 초대 링크들은 강제로 만료될 것입니다. + invite_max_uses: 설정한 경우 지정한 수 이하의 최대 사용횟수를 가진 초대 링크는 강제로 만료되지 않을 것입니다. mascot: 고급 웹 인터페이스의 그림을 대체합니다. peers_api_enabled: 이 서버가 연합우주에서 만났던 서버들에 대한 도메인 네임의 목록입니다. 해당 서버와 어떤 연합을 했는지에 대한 정보는 전혀 포함되지 않고, 단순히 그 서버를 알고 있는지에 대한 것입니다. 이것은 일반적으로 연합에 대한 통계를 수집할 때 사용됩니다. profile_directory: 프로필 책자는 발견되기를 희망하는 모든 사람들의 목록을 나열합니다. @@ -241,6 +243,8 @@ ko: bootstrap_timeline_accounts: 새로운 사용자들에게 추천할 계정들 closed_registrations_message: 가입이 불가능 할 때의 사용자 지정 메시지 custom_css: 사용자 정의 CSS + invite_expiration_period: 초대 링크 강제 만료 기한 + invite_max_uses: 초대 링크 강제 만료 최대 사용횟수 임계값 mascot: 사용자 정의 마스코트 (legacy) media_cache_retention_period: 미디어 캐시 유지 기한 peers_api_enabled: API에 발견 된 서버들의 목록 발행 diff --git a/spec/lib/vacuum/invites_vacuum_spec.rb b/spec/lib/vacuum/invites_vacuum_spec.rb new file mode 100644 index 0000000000..b6c03f8330 --- /dev/null +++ b/spec/lib/vacuum/invites_vacuum_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Vacuum::InvitesVacuum do + subject { described_class.new(retention_period, retention_max_uses) } + + let(:retention_period) { 7.days } + let(:retention_max_uses) { 10 } + + let(:user) { Fabricate(:user) } + + describe '#perform' do + let!(:invite_unlimited) { Fabricate(:invite, user: user, max_uses: nil, created_at: 10.days.ago, expires_at: nil) } + let!(:invite_huge_max_uses) { Fabricate(:invite, max_uses: 100, created_at: 10.days.ago, expires_at: nil) } + let!(:invite_small_max_uses) { Fabricate(:invite, max_uses: 2, created_at: 10.days.ago, expires_at: nil) } + let!(:invite_will_expires_later) { Fabricate(:invite, max_uses: nil, created_at: 1.hour.ago, expires_at: 1.hour.from_now) } + + before do + subject.perform + end + + it 'expires unlimited invitation link' do + expect(invite_unlimited.reload.expired?).to be true + end + + it 'expires invitation link that have huge max uses' do + expect(invite_huge_max_uses.reload.expired?).to be true + end + + it 'does not expires invitation link that have small max uses' do + expect(invite_small_max_uses.reload.expired?).to be false + end + + it 'expires invitation link that will expire' do + expect(invite_will_expires_later.reload.expired?).to be false + end + end +end