Compare commits
9 commits
76b36ba10d
...
166d412414
Author | SHA1 | Date | |
---|---|---|---|
166d412414 | |||
|
3644101a79 | ||
|
d6a8c69b5c | ||
|
26e086cd0b | ||
|
ecc917008e | ||
|
d7d477047e | ||
|
6c1b6194f7 | ||
|
63080e10a9 | ||
|
e7ca82762d |
27 changed files with 430 additions and 51 deletions
|
@ -19,6 +19,7 @@
|
||||||
.github
|
.github
|
||||||
.gitignore
|
.gitignore
|
||||||
.woodpecker.yml
|
.woodpecker.yml
|
||||||
|
/*.md
|
||||||
build
|
build
|
||||||
chart
|
chart
|
||||||
coverage
|
coverage
|
||||||
|
|
95
CHANGELOG.md
95
CHANGELOG.md
|
@ -2,6 +2,101 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [4.2.7] - 2024-02-16
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix OmniAuth tests and edge cases in error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29201), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29207))
|
||||||
|
- Fix new installs by upgrading to the latest release of the `nsa` gem, instead of a no longer existing commit ([mjankowski](https://github.com/mastodon/mastodon/pull/29065))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix insufficient checking of remote posts ([GHSA-jhrq-qvrm-qr36](https://github.com/mastodon/mastodon/security/advisories/GHSA-jhrq-qvrm-qr36))
|
||||||
|
|
||||||
|
## [4.2.6] - 2024-02-14
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Update the `sidekiq-unique-jobs` dependency (see [GHSA-cmh9-rx85-xj38](https://github.com/mhenrixon/sidekiq-unique-jobs/security/advisories/GHSA-cmh9-rx85-xj38))
|
||||||
|
In addition, we have disabled the web interface for `sidekiq-unique-jobs` out of caution.
|
||||||
|
If you need it, you can re-enable it by setting `ENABLE_SIDEKIQ_UNIQUE_JOBS_UI=true`.
|
||||||
|
If you only need to clear all locks, you can now use `bundle exec rake sidekiq_unique_jobs:delete_all_locks`.
|
||||||
|
- Update the `nokogiri` dependency (see [GHSA-xc9x-jj77-9p9j](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j))
|
||||||
|
- Disable administrative Doorkeeper routes ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29187))
|
||||||
|
- Fix ongoing streaming sessions not being invalidated when applications get deleted in some cases ([GHSA-7w3c-p9j8-mq3x](https://github.com/mastodon/mastodon/security/advisories/GHSA-7w3c-p9j8-mq3x))
|
||||||
|
In some rare cases, the streaming server was not notified of access tokens revocation on application deletion.
|
||||||
|
- Change external authentication behavior to never reattach a new identity to an existing user by default ([GHSA-vm39-j3vx-pch3](https://github.com/mastodon/mastodon/security/advisories/GHSA-vm39-j3vx-pch3))
|
||||||
|
Up until now, Mastodon has allowed new identities from external authentication providers to attach to an existing local user based on their verified e-mail address.
|
||||||
|
This allowed upgrading users from a database-stored password to an external authentication provider, or move from one authentication provider to another.
|
||||||
|
However, this behavior may be unexpected, and means that when multiple authentication providers are configured, the overall security would be that of the least secure authentication provider.
|
||||||
|
For these reasons, this behavior is now locked under the `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH` environment variable.
|
||||||
|
In addition, regardless of this environment variable, Mastodon will refuse to attach two identities from the same authentication provider to the same account.
|
||||||
|
|
||||||
|
## [4.2.5] - 2024-02-01
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix insufficient origin validation (CVE-2024-23832, [GHSA-3fjr-858r-92rw](https://github.com/mastodon/mastodon/security/advisories/GHSA-3fjr-858r-92rw))
|
||||||
|
|
||||||
|
## [4.2.4] - 2024-01-24
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix error when processing remote files with unusually long names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28823))
|
||||||
|
- Fix processing of compacted single-item JSON-LD collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28816))
|
||||||
|
- Retry 401 errors on replies fetching ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28788))
|
||||||
|
- Fix `RecordNotUnique` errors in LinkCrawlWorker ([tribela](https://github.com/mastodon/mastodon/pull/28748))
|
||||||
|
- Fix Mastodon not correctly processing HTTP Signatures with query strings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28443), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28476))
|
||||||
|
- Fix potential redirection loop of streaming endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28665))
|
||||||
|
- Fix streaming API redirection ignoring the port of `streaming_api_base_url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28558))
|
||||||
|
- Fix error when processing link preview with an array as `inLanguage` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28252))
|
||||||
|
- Fix unsupported time zone or locale preventing sign-up ([Gargron](https://github.com/mastodon/mastodon/pull/28035))
|
||||||
|
- Fix "Hide these posts from home" list setting not refreshing when switching lists ([brianholley](https://github.com/mastodon/mastodon/pull/27763))
|
||||||
|
- Fix missing background behind dismissable banner in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/27479))
|
||||||
|
- Fix line wrapping of language selection button with long locale codes ([gunchleoc](https://github.com/mastodon/mastodon/pull/27100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27127))
|
||||||
|
- Fix `Undo Announce` activity not being sent to non-follower authors ([MitarashiDango](https://github.com/mastodon/mastodon/pull/18482))
|
||||||
|
- Fix N+1s because of association preloaders not actually getting called ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28339))
|
||||||
|
- Fix empty column explainer getting cropped under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28337))
|
||||||
|
- Fix `LinkCrawlWorker` error when encountering empty OEmbed response ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28268))
|
||||||
|
- Fix call to inefficient `delete_matched` cache method in domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28367))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Add rate-limit of TOTP authentication attempts at controller level ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28801))
|
||||||
|
|
||||||
|
## [4.2.3] - 2023-12-05
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix dependency on `json-canonicalization` version that has been made unavailable since last release
|
||||||
|
|
||||||
|
## [4.2.2] - 2023-12-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change dismissed banners to be stored server-side ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27055))
|
||||||
|
- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927))
|
||||||
|
- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586))
|
||||||
|
- Change single-column navigation notice to be displayed outside of the logo container ([renchap](https://github.com/mastodon/mastodon/pull/27462), [renchap](https://github.com/mastodon/mastodon/pull/27476))
|
||||||
|
- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889))
|
||||||
|
- Change post language code to include country code when relevant ([gunchleoc](https://github.com/mastodon/mastodon/pull/27099), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27207))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix upper border radius of onboarding columns ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27890))
|
||||||
|
- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081))
|
||||||
|
- Fix some posts from threads received out-of-order sometimes not being inserted into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27653))
|
||||||
|
- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620))
|
||||||
|
- Fix error when trying to delete already-deleted file with OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27569))
|
||||||
|
- Fix batch attachment deletion when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27554))
|
||||||
|
- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474))
|
||||||
|
- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459))
|
||||||
|
- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442))
|
||||||
|
- Fix handling of `inLanguage` attribute in preview card processing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27423))
|
||||||
|
- Fix own posts being removed from home timeline when unfollowing a used hashtag ([kmycode](https://github.com/mastodon/mastodon/pull/27391))
|
||||||
|
- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584))
|
||||||
|
- Fix format-dependent redirects being cached regardless of requested format ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27634))
|
||||||
|
|
||||||
## [4.2.1] - 2023-10-10
|
## [4.2.1] - 2023-10-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
14
DIVERGENCES.md
Normal file
14
DIVERGENCES.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# Divergences
|
||||||
|
|
||||||
|
## Major Features
|
||||||
|
|
||||||
|
- quote posting
|
||||||
|
- Treehouse::Automod (experimental feature flagged)
|
||||||
|
|
||||||
|
## Other Changes
|
||||||
|
|
||||||
|
- various build system changes
|
||||||
|
- a better dockerfile
|
||||||
|
- yarn v2 (a mistake, tbh)
|
||||||
|
- various dev env changes
|
||||||
|
- various css/style changes
|
|
@ -506,7 +506,7 @@ GEM
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.5.4)
|
pg (1.5.5)
|
||||||
pghero (3.4.1)
|
pghero (3.4.1)
|
||||||
activerecord (>= 6)
|
activerecord (>= 6)
|
||||||
posix-spawn (0.3.15)
|
posix-spawn (0.3.15)
|
||||||
|
|
|
@ -17,6 +17,9 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
session["devise.#{provider}_data"] = request.env['omniauth.auth']
|
session["devise.#{provider}_data"] = request.env['omniauth.auth']
|
||||||
redirect_to new_user_registration_url
|
redirect_to new_user_registration_url
|
||||||
end
|
end
|
||||||
|
rescue ActiveRecord::RecordInvalid
|
||||||
|
flash[:alert] = I18n.t('devise.failure.omniauth_user_creation_failure') if is_navigational_format?
|
||||||
|
redirect_to new_user_session_url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -174,7 +174,19 @@ module JsonLdHelper
|
||||||
build_request(uri, on_behalf_of, options: request_options).perform do |response|
|
build_request(uri, on_behalf_of, options: request_options).perform do |response|
|
||||||
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
|
||||||
|
|
||||||
body_to_json(response.body_with_limit) if response.code == 200
|
body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_activitypub_content_type?(response)
|
||||||
|
return true if response.mime_type == 'application/activity+json'
|
||||||
|
|
||||||
|
# When the mime type is `application/ld+json`, we need to check the profile,
|
||||||
|
# but `http.rb` does not parse it for us.
|
||||||
|
return false unless response.mime_type == 'application/ld+json'
|
||||||
|
|
||||||
|
response.headers[HTTP::Headers::CONTENT_TYPE]&.split(';')&.map(&:strip)&.any? do |str|
|
||||||
|
str.start_with?('profile="') && str[9...-1].split.include?('https://www.w3.org/ns/activitystreams')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
|
|
||||||
resolve_thread(@status)
|
resolve_thread(@status)
|
||||||
fetch_replies(@status)
|
fetch_replies(@status)
|
||||||
|
return if Treehouse::Automod.process_status!(@status)
|
||||||
distribute
|
distribute
|
||||||
forward_for_reply
|
forward_for_reply
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'digest'
|
||||||
|
|
||||||
class ActivityPub::ProcessAccountService < BaseService
|
class ActivityPub::ProcessAccountService < BaseService
|
||||||
include JsonLdHelper
|
include JsonLdHelper
|
||||||
include DomainControlHelper
|
include DomainControlHelper
|
||||||
|
@ -90,6 +92,9 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
set_immediate_protocol_attributes!
|
set_immediate_protocol_attributes!
|
||||||
set_fetchable_key! unless @account.suspended? && @account.suspension_origin_local?
|
set_fetchable_key! unless @account.suspended? && @account.suspension_origin_local?
|
||||||
set_immediate_attributes! unless @account.suspended?
|
set_immediate_attributes! unless @account.suspended?
|
||||||
|
|
||||||
|
Treehouse::Automod.process_account!(@account)
|
||||||
|
|
||||||
set_fetchable_attributes! unless @options[:only_key] || @account.suspended?
|
set_fetchable_attributes! unless @options[:only_key] || @account.suspended?
|
||||||
|
|
||||||
@account.save_with_optional_media!
|
@account.save_with_optional_media!
|
||||||
|
|
|
@ -44,7 +44,7 @@ class FetchResourceService < BaseService
|
||||||
@response_code = response.code
|
@response_code = response.code
|
||||||
return nil if response.code != 200
|
return nil if response.code != 200
|
||||||
|
|
||||||
if ['application/activity+json', 'application/ld+json'].include?(response.mime_type)
|
if valid_activitypub_content_type?(response)
|
||||||
body = response.body_with_limit
|
body = response.body_with_limit
|
||||||
json = body_to_json(body)
|
json = body_to_json(body)
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ class ReportService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_staff!
|
def notify_staff!
|
||||||
|
return if @options[:th_skip_notify_staff]
|
||||||
return if @report.unresolved_siblings?
|
return if @report.unresolved_siblings?
|
||||||
|
|
||||||
User.those_who_can(:manage_reports).includes(:account).find_each do |u|
|
User.those_who_can(:manage_reports).includes(:account).find_each do |u|
|
||||||
|
@ -65,6 +66,7 @@ class ReportService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def forward?
|
def forward?
|
||||||
|
return false if @options[:th_skip_forward]
|
||||||
!@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
!@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,8 @@ require_relative '../lib/active_record/batches'
|
||||||
require_relative '../lib/simple_navigation/item_extensions'
|
require_relative '../lib/simple_navigation/item_extensions'
|
||||||
require_relative '../lib/http_extensions'
|
require_relative '../lib/http_extensions'
|
||||||
|
|
||||||
|
require_relative '../lib/treehouse/automod'
|
||||||
|
|
||||||
Dotenv::Railtie.load
|
Dotenv::Railtie.load
|
||||||
|
|
||||||
Bundler.require(:pam_authentication) if ENV['PAM_ENABLED'] == 'true'
|
Bundler.require(:pam_authentication) if ENV['PAM_ENABLED'] == 'true'
|
||||||
|
@ -107,5 +109,9 @@ module Mastodon
|
||||||
Devise::FailureApp.include AbstractController::Callbacks
|
Devise::FailureApp.include AbstractController::Callbacks
|
||||||
Devise::FailureApp.include Localized
|
Devise::FailureApp.include Localized
|
||||||
end
|
end
|
||||||
|
|
||||||
|
config.x.th_automod.automod_account_username = ENV['TH_STAFF_ACCOUNT']
|
||||||
|
config.x.th_automod.account_service_heuristic_auto_suspend_active = ENV.fetch('TH_ACCOUNT_SERVICE_HEURISTIC_AUTO_SUSPEND', '') == 'that-one-spammer'
|
||||||
|
config.x.th_automod.mention_spam_heuristic_auto_limit_active = ENV.fetch('TH_MENTION_SPAM_HEURISTIC_AUTO_LIMIT_ACTIVE', '') == 'can-spam'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,6 +12,7 @@ en:
|
||||||
last_attempt: You have one more attempt before your account is locked.
|
last_attempt: You have one more attempt before your account is locked.
|
||||||
locked: Your account is locked.
|
locked: Your account is locked.
|
||||||
not_found_in_database: Invalid %{authentication_keys} or password.
|
not_found_in_database: Invalid %{authentication_keys} or password.
|
||||||
|
omniauth_user_creation_failure: Error creating an account for this identity.
|
||||||
pending: Your account is still under review.
|
pending: Your account is still under review.
|
||||||
timeout: Your session expired. Please login again to continue.
|
timeout: Your session expired. Please login again to continue.
|
||||||
unauthenticated: You need to login or sign up before continuing.
|
unauthenticated: You need to login or sign up before continuing.
|
||||||
|
|
|
@ -56,7 +56,7 @@ services:
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.2.0
|
image: ghcr.io/mastodon/mastodon:v4.2.7
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec puma -C config/puma.rb
|
command: bundle exec puma -C config/puma.rb
|
||||||
|
@ -77,7 +77,7 @@ services:
|
||||||
|
|
||||||
streaming:
|
streaming:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.2.0
|
image: ghcr.io/mastodon/mastodon:v4.2.7
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: node ./streaming
|
command: node ./streaming
|
||||||
|
@ -95,7 +95,7 @@ services:
|
||||||
|
|
||||||
sidekiq:
|
sidekiq:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.2.0
|
image: ghcr.io/mastodon/mastodon:v4.2.7
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
|
|
|
@ -17,7 +17,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_prerelease
|
def default_prerelease
|
||||||
'alpha.2'
|
'alpha.3'
|
||||||
end
|
end
|
||||||
|
|
||||||
def prerelease
|
def prerelease
|
||||||
|
|
128
lib/treehouse/automod.rb
Normal file
128
lib/treehouse/automod.rb
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
module Treehouse
|
||||||
|
module Automod
|
||||||
|
COMMENT_HEADER = <<~EOS
|
||||||
|
Tracking Report - automatically created by TreehouseAutomod
|
||||||
|
EOS
|
||||||
|
|
||||||
|
WARNING_TEXT = <<~EOS
|
||||||
|
Tracking Infraction - automatically created by TreehouseAutomod
|
||||||
|
EOS
|
||||||
|
|
||||||
|
def self.suspend_with_tracking_report!(account, status_ids: [], explanation: "")
|
||||||
|
account.save!
|
||||||
|
|
||||||
|
self.file_tracking_report!(account, status_ids: status_ids) unless account.suspension_origin == "local"
|
||||||
|
|
||||||
|
account.suspend! unless account.suspension_origin == "local"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.file_tracking_report!(account, status_ids: [], explanation: "")
|
||||||
|
reporter = self.staff_account
|
||||||
|
return if reporter.nil?
|
||||||
|
|
||||||
|
report = ReportService.new.call(
|
||||||
|
reporter,
|
||||||
|
account,
|
||||||
|
{
|
||||||
|
status_ids: status_ids,
|
||||||
|
comment: explanation.blank? ? COMMENT_HEADER : "#{COMMENT_HEADER}\n\n#{EXPLANATION}",
|
||||||
|
th_skip_notify_staff: true,
|
||||||
|
th_skip_forward: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
report.spam!
|
||||||
|
report.assign_to_self!(reporter)
|
||||||
|
|
||||||
|
account_action = Admin::AccountAction.new(
|
||||||
|
type: "suspend",
|
||||||
|
report_id: report.id,
|
||||||
|
target_account: account,
|
||||||
|
current_account: reporter,
|
||||||
|
send_email_notification: false,
|
||||||
|
text: WARNING_TEXT,
|
||||||
|
)
|
||||||
|
account_action.save!
|
||||||
|
|
||||||
|
report.resolve!(reporter)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.staff_account
|
||||||
|
username = Rails.configuration.x.th_automod.automod_account_username
|
||||||
|
Account.find_local(username) unless username.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.process_status!(status)
|
||||||
|
ActivityPubActivityCreateExt.process!(status)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.process_account!(account)
|
||||||
|
AccountServiceExt.process!(account)
|
||||||
|
end
|
||||||
|
|
||||||
|
module ActivityPubActivityCreateExt
|
||||||
|
EXPLANATION = <<~EOS
|
||||||
|
This account was automatically suspended by TreehouseAutomod, an unsupported feature.
|
||||||
|
|
||||||
|
Currently, the account-only heuristic should only automatically suspend accounts with one specific username and display name.
|
||||||
|
|
||||||
|
If this action is unexpected, please unset TH_MENTION_SPAM_HEURISTIC_AUTO_LIMIT_ACTIVE.
|
||||||
|
EOS
|
||||||
|
|
||||||
|
# check if the status should be considered spam
|
||||||
|
# @return true if the status was reported and the account was infracted
|
||||||
|
def process!(status)
|
||||||
|
return false unless Rails.configuration.x.th_automod.mention_spam_heuristic_auto_limit_active
|
||||||
|
account = status.account
|
||||||
|
minimal_effort = account.note.blank? && account.avatar_remote_url.blank? && account.header_remote_url.blank?
|
||||||
|
return false if (account.local? ||
|
||||||
|
account.local_followers_account > 0 ||
|
||||||
|
!minimal_effort)
|
||||||
|
|
||||||
|
# minimal effort account, check mentions and account-known age
|
||||||
|
status.mentions.size > 8 && account.created_at > (Time.now - 1.day)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module AccountServiceExt
|
||||||
|
# hardcoded for now
|
||||||
|
# md5 because they don't deserve more mentions
|
||||||
|
HEURISTIC_NAMES = {
|
||||||
|
"0116a9deace3289b7092e945ef5ca0a5" => Set["57d3d0b932cc9cd01be6b2f4e82c1a4a"],
|
||||||
|
}
|
||||||
|
# probably mathematically impossible to collide, but just in case...
|
||||||
|
HEURISTIC_MAX_LEN = 16
|
||||||
|
|
||||||
|
EXPLANATION = <<~EOS
|
||||||
|
This account was automatically suspended by TreehouseAutomod, an unsupported feature.
|
||||||
|
|
||||||
|
Currently, the account-only heuristic should only automatically suspend accounts with one specific username and display name.
|
||||||
|
|
||||||
|
If this action is unexpected, please unset TH_HEURISTIC_AUTO_SUSPEND.
|
||||||
|
EOS
|
||||||
|
|
||||||
|
# @return true if the account was infracted
|
||||||
|
def self.process!(account)
|
||||||
|
return false unless heuristic_auto_suspend?(account)
|
||||||
|
|
||||||
|
Automod.suspend_with_tracking_report!(account, explanation: EXPLANATION) unless account.suspension_origin == "local"
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.matches_evil_hash?(account)
|
||||||
|
username_md5 = Digest::MD5.hexdigest(account.username)
|
||||||
|
display_name_md5 = Digest::MD5.hexdigest(account.display_name)
|
||||||
|
|
||||||
|
HEURISTIC_NAMES[username_md5].include?(display_name_md5)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.heuristic_auto_suspend?(account)
|
||||||
|
return false unless Rails.configuration.x.th_automod.account_service_heuristic_auto_suspend_active
|
||||||
|
|
||||||
|
return unless account.username.length < HEURISTIC_MAX_LEN && account.display_name.length < HEURISTIC_MAX_LEN
|
||||||
|
|
||||||
|
self.matches_evil_hash?(account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -56,15 +56,15 @@ describe JsonLdHelper do
|
||||||
describe '#fetch_resource' do
|
describe '#fetch_resource' do
|
||||||
context 'when the second argument is false' do
|
context 'when the second argument is false' do
|
||||||
it 'returns resource even if the retrieved ID and the given URI does not match' do
|
it 'returns resource even if the retrieved ID and the given URI does not match' do
|
||||||
stub_request(:get, 'https://bob.test/').to_return body: '{"id": "https://alice.test/"}'
|
stub_request(:get, 'https://bob.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://alice.test/').to_return body: '{"id": "https://alice.test/"}'
|
stub_request(:get, 'https://alice.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
|
||||||
expect(fetch_resource('https://bob.test/', false)).to eq({ 'id' => 'https://alice.test/' })
|
expect(fetch_resource('https://bob.test/', false)).to eq({ 'id' => 'https://alice.test/' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns nil if the object identified by the given URI and the object identified by the retrieved ID does not match' do
|
it 'returns nil if the object identified by the given URI and the object identified by the retrieved ID does not match' do
|
||||||
stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://marvin.test/"}'
|
stub_request(:get, 'https://mallory.test/').to_return(body: '{"id": "https://marvin.test/"}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://marvin.test/').to_return body: '{"id": "https://alice.test/"}'
|
stub_request(:get, 'https://marvin.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
|
||||||
expect(fetch_resource('https://mallory.test/', false)).to be_nil
|
expect(fetch_resource('https://mallory.test/', false)).to be_nil
|
||||||
end
|
end
|
||||||
|
@ -72,7 +72,7 @@ describe JsonLdHelper do
|
||||||
|
|
||||||
context 'when the second argument is true' do
|
context 'when the second argument is true' do
|
||||||
it 'returns nil if the retrieved ID and the given URI does not match' do
|
it 'returns nil if the retrieved ID and the given URI does not match' do
|
||||||
stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://alice.test/"}'
|
stub_request(:get, 'https://mallory.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
expect(fetch_resource('https://mallory.test/', true)).to be_nil
|
expect(fetch_resource('https://mallory.test/', true)).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -80,12 +80,12 @@ describe JsonLdHelper do
|
||||||
|
|
||||||
describe '#fetch_resource_without_id_validation' do
|
describe '#fetch_resource_without_id_validation' do
|
||||||
it 'returns nil if the status code is not 200' do
|
it 'returns nil if the status code is not 200' do
|
||||||
stub_request(:get, 'https://host.test/').to_return status: 400, body: '{}'
|
stub_request(:get, 'https://host.test/').to_return(status: 400, body: '{}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
expect(fetch_resource_without_id_validation('https://host.test/')).to be_nil
|
expect(fetch_resource_without_id_validation('https://host.test/')).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns hash' do
|
it 'returns hash' do
|
||||||
stub_request(:get, 'https://host.test/').to_return status: 200, body: '{}'
|
stub_request(:get, 'https://host.test/').to_return(status: 200, body: '{}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
expect(fetch_resource_without_id_validation('https://host.test/')).to eq({})
|
expect(fetch_resource_without_id_validation('https://host.test/')).to eq({})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,7 +35,7 @@ RSpec.describe ActivityPub::Activity::Announce do
|
||||||
context 'when sender is followed by a local account' do
|
context 'when sender is followed by a local account' do
|
||||||
before do
|
before do
|
||||||
Fabricate(:account).follow!(sender)
|
Fabricate(:account).follow!(sender)
|
||||||
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
|
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
subject.perform
|
subject.perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ RSpec.describe ActivityPub::Activity::Announce do
|
||||||
let(:object_json) { 'https://example.com/actor/hello-world' }
|
let(:object_json) { 'https://example.com/actor/hello-world' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
|
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the relay is enabled' do
|
context 'when the relay is enabled' do
|
||||||
|
|
|
@ -60,11 +60,13 @@ describe 'OmniAuth callbacks' do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH is not set to true' do
|
context 'when ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH is not set to true' do
|
||||||
it 'does not match the existing user or create an identity' do
|
it 'does not match the existing user or create an identity, and redirects to login page' do
|
||||||
expect { subject }
|
expect { subject }
|
||||||
.to not_change(User, :count)
|
.to not_change(User, :count)
|
||||||
.and not_change(Identity, :count)
|
.and not_change(Identity, :count)
|
||||||
.and not_change(LoginActivity, :count)
|
.and not_change(LoginActivity, :count)
|
||||||
|
|
||||||
|
expect(response).to redirect_to(new_user_session_url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -72,11 +72,11 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
||||||
|
|
||||||
shared_examples 'sets pinned posts' do
|
shared_examples 'sets pinned posts' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known))
|
stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined))
|
stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404)
|
stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404)
|
||||||
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable))
|
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: Oj.dump(featured_with_null))
|
stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: Oj.dump(featured_with_null), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
|
||||||
subject.call(actor, note: true, hashtag: false)
|
subject.call(actor, note: true, hashtag: false)
|
||||||
end
|
end
|
||||||
|
@ -94,7 +94,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
context 'when the endpoint is a Collection' do
|
context 'when the endpoint is a Collection' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'sets pinned posts'
|
it_behaves_like 'sets pinned posts'
|
||||||
|
@ -111,7 +111,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'sets pinned posts'
|
it_behaves_like 'sets pinned posts'
|
||||||
|
@ -120,7 +120,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
||||||
let(:items) { 'https://example.com/account/pinned/unknown-reachable' }
|
let(:items) { 'https://example.com/account/pinned/unknown-reachable' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable))
|
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
subject.call(actor, note: true, hashtag: false)
|
subject.call(actor, note: true, hashtag: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'sets pinned posts'
|
it_behaves_like 'sets pinned posts'
|
||||||
|
@ -156,7 +156,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
||||||
let(:items) { 'https://example.com/account/pinned/unknown-reachable' }
|
let(:items) { 'https://example.com/account/pinned/unknown-reachable' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable))
|
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
subject.call(actor, note: true, hashtag: false)
|
subject.call(actor, note: true, hashtag: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
context 'when the endpoint is a Collection' do
|
context 'when the endpoint is a Collection' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'sets featured tags'
|
it_behaves_like 'sets featured tags'
|
||||||
|
@ -46,7 +46,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d
|
||||||
|
|
||||||
context 'when the account already has featured tags' do
|
context 'when the account already has featured tags' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
|
||||||
actor.featured_tags.create!(name: 'FoO')
|
actor.featured_tags.create!(name: 'FoO')
|
||||||
actor.featured_tags.create!(name: 'baz')
|
actor.featured_tags.create!(name: 'baz')
|
||||||
|
@ -67,7 +67,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'sets featured tags'
|
it_behaves_like 'sets featured tags'
|
||||||
|
@ -88,7 +88,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'sets featured tags'
|
it_behaves_like 'sets featured tags'
|
||||||
|
|
|
@ -44,7 +44,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
|
||||||
before do
|
before do
|
||||||
actor[:inbox] = nil
|
actor[:inbox] = nil
|
||||||
|
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
end
|
end
|
||||||
|
@ -125,7 +125,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -148,7 +148,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,7 +44,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
|
||||||
before do
|
before do
|
||||||
actor[:inbox] = nil
|
actor[:inbox] = nil
|
||||||
|
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
end
|
end
|
||||||
|
@ -125,7 +125,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -148,7 +148,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
end
|
end
|
||||||
|
|
|
@ -50,7 +50,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do
|
||||||
|
|
||||||
context 'when the key is a sub-object from the actor' do
|
context 'when the key is a sub-object from the actor' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, public_key_id).to_return(body: Oj.dump(actor))
|
stub_request(:get, public_key_id).to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the expected account' do
|
it 'returns the expected account' do
|
||||||
|
@ -71,7 +71,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do
|
||||||
let(:public_key_id) { 'https://example.com/alice-public-key.json' }
|
let(:public_key_id) { 'https://example.com/alice-public-key.json' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })))
|
stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the expected account' do
|
it 'returns the expected account' do
|
||||||
|
@ -84,7 +84,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do
|
||||||
let(:actor_public_key) { 'https://example.com/alice-public-key.json' }
|
let(:actor_public_key) { 'https://example.com/alice-public-key.json' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })))
|
stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the nil' do
|
it 'returns the nil' do
|
||||||
|
|
|
@ -58,7 +58,7 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do
|
||||||
|
|
||||||
context 'when passing the URL to the collection' do
|
context 'when passing the URL to the collection' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'spawns workers for up to 5 replies on the same server' do
|
it 'spawns workers for up to 5 replies on the same server' do
|
||||||
|
@ -93,7 +93,7 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do
|
||||||
|
|
||||||
context 'when passing the URL to the collection' do
|
context 'when passing the URL to the collection' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'spawns workers for up to 5 replies on the same server' do
|
it 'spawns workers for up to 5 replies on the same server' do
|
||||||
|
@ -132,7 +132,7 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do
|
||||||
|
|
||||||
context 'when passing the URL to the collection' do
|
context 'when passing the URL to the collection' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'spawns workers for up to 5 replies on the same server' do
|
it 'spawns workers for up to 5 replies on the same server' do
|
||||||
|
|
|
@ -200,6 +200,114 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'treehouse automod' do
|
||||||
|
subject { described_class.new.call(account_username, 'foo.test', payload) }
|
||||||
|
let(:account_username) { 'evil' }
|
||||||
|
let(:account_display_name) { 'evil display name' }
|
||||||
|
let(:account_payload_suspended) { false }
|
||||||
|
|
||||||
|
let(:automod_account_username) { nil }
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
id: 'https://foo.test',
|
||||||
|
type: 'Actor',
|
||||||
|
inbox: 'https://foo.test/inbox',
|
||||||
|
suspended: account_payload_suspended,
|
||||||
|
name: account_display_name,
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:name_hash_hash) do
|
||||||
|
{
|
||||||
|
# 'evil' => 'evil display name'
|
||||||
|
'4034a346ccee15292d823416f7510a2f' => Set['225e44a7c4a792ee22a4ada2032da7cd']
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Rails.configuration.x.th_automod).to receive(:account_service_heuristic_auto_suspend_active).and_return(true)
|
||||||
|
allow(Rails.configuration.x.th_automod).to receive(:automod_account_username).and_return(automod_account_username)
|
||||||
|
|
||||||
|
stub_const('::Treehouse::Automod::AccountServiceExt::HEURISTIC_NAMES', name_hash_hash)
|
||||||
|
stub_const('::Treehouse::Automod::AccountServiceExt::HEURISTIC_MAX_LEN', 20)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'new account' do
|
||||||
|
context 'heuristic matching' do
|
||||||
|
it 'suspends the user locally' do
|
||||||
|
expect(subject.suspended?).to be true
|
||||||
|
expect(subject.suspension_origin_local?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'heuristic not matching' do
|
||||||
|
let(:account_display_name) { '' }
|
||||||
|
it 'does nothing' do
|
||||||
|
expect(subject.suspended?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'existing account' do
|
||||||
|
let!(:account) { Fabricate(:account, username: account_username, domain: 'foo.test', display_name: account_display_name) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Admin::SuspensionWorker).to receive(:perform_async)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'heuristic matching' do
|
||||||
|
it 'suspends the user locally' do
|
||||||
|
expect(subject.suspended?).to be true
|
||||||
|
expect(subject.suspension_origin_local?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'heuristic not matching' do
|
||||||
|
let(:account_display_name) { 'not evil display name' }
|
||||||
|
|
||||||
|
it 'does nothing' do
|
||||||
|
expect(subject.suspended?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'suspended locally' do
|
||||||
|
before do
|
||||||
|
account.suspend!(origin: :local)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does nothing' do
|
||||||
|
expect(subject.suspended?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'tracking report' do
|
||||||
|
let(:automod_account_username) { 'automod_test' }
|
||||||
|
|
||||||
|
let!(:automod_user_role) { Fabricate(:user_role, name: 'Automod', permissions: UserRole::FLAGS[:administrator]) }
|
||||||
|
|
||||||
|
let!(:automod_account) do
|
||||||
|
account = Fabricate(:account, username: automod_account_username)
|
||||||
|
account.user.role_id = automod_user_role.id
|
||||||
|
account.user.save!
|
||||||
|
account
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates report' do
|
||||||
|
expect(subject.targeted_reports.empty?).to be_falsy
|
||||||
|
|
||||||
|
report = Report.find_by(target_account_id: subject.id, account_id: automod_account.id, assigned_account_id: automod_account.id)
|
||||||
|
expect(report.comment.starts_with?('Tracking Report - automatically created by TreehouseAutomod')).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates account action' do
|
||||||
|
subject
|
||||||
|
expect(Admin::ActionLog.find_by(account_id: automod_account.id, target_id: subject.id)).not_to be nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_some_remote_accounts
|
def create_some_remote_accounts
|
||||||
|
@ -209,4 +317,5 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
|
||||||
def create_fewer_than_rate_limit_accounts
|
def create_fewer_than_rate_limit_accounts
|
||||||
change(Account.remote, :count).by_at_most(5)
|
change(Account.remote, :count).by_at_most(5)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -60,7 +60,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
context 'when the endpoint is a Collection of actor URIs' do
|
context 'when the endpoint is a Collection of actor URIs' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'synchronizes followers'
|
it_behaves_like 'synchronizes followers'
|
||||||
|
@ -77,7 +77,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'synchronizes followers'
|
it_behaves_like 'synchronizes followers'
|
||||||
|
@ -98,7 +98,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'synchronizes followers'
|
it_behaves_like 'synchronizes followers'
|
||||||
|
|
|
@ -21,7 +21,7 @@ describe ActivityPub::FetchRepliesWorker do
|
||||||
|
|
||||||
describe 'perform' do
|
describe 'perform' do
|
||||||
it 'performs a request if the collection URI is from the same host' do
|
it 'performs a request if the collection URI is from the same host' do
|
||||||
stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 200, body: json)
|
stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 200, body: json, headers: { 'Content-Type': 'application/activity+json' })
|
||||||
subject.perform(status.id, 'https://example.com/statuses_replies/1')
|
subject.perform(status.id, 'https://example.com/statuses_replies/1')
|
||||||
expect(a_request(:get, 'https://example.com/statuses_replies/1')).to have_been_made.once
|
expect(a_request(:get, 'https://example.com/statuses_replies/1')).to have_been_made.once
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue