mirror of
https://git.kescher.at/CatCatNya/catstodon.git
synced 2024-11-22 10:38:07 +01:00
Merge commit '491dd9764244c8adf37861f00d916c96bdbfdaf8' into glitch-soc/merge-upstream
Conflicts: - `app/workers/scheduler/auto_close_registrations_scheduler.rb`: Changes were already cherry-picked and updated further in glitch-soc. Kept glitch-soc's version.
This commit is contained in:
commit
7901dc9e24
31 changed files with 2270 additions and 1848 deletions
|
@ -1,4 +1 @@
|
||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
yarn lint-staged
|
yarn lint-staged
|
||||||
|
|
|
@ -966,6 +966,9 @@ da:
|
||||||
title: Webhooks
|
title: Webhooks
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: Grundet manglende nylig moderatoraktivitet er registreringsproceduren på %{instance} automatisk ændret til at kræve manuel gennemgang for at forhindre, at %{instance} bruges som platform for potentielle dårlige aktører. Proceduren kan til enhver tid ændre igen til åbne registreringer.
|
||||||
|
subject: Registreringsproceduren for %{instance} er automatisk ændret til at kræve godkendelse
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: for sletning af vedkommendes indlæg
|
delete_statuses: for sletning af vedkommendes indlæg
|
||||||
|
|
|
@ -966,6 +966,9 @@ de:
|
||||||
title: Webhooks
|
title: Webhooks
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: Aufgrund fehlender Aktivität von Moderator*innen müssen neue Registrierungen auf %{instance} jetzt manuell genehmigt werden. Dies wurde automatisch umgestellt, damit %{instance} nicht als Plattform für Böswillige missbraucht werden kann. Du kannst jederzeit auf uneingeschränkte Registrierungen zurückwechseln.
|
||||||
|
subject: Registrierungen auf %{instance} erfordern jetzt eine manuelle Genehmigung (automatisch umgestellt)
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: das Löschen der Beiträge
|
delete_statuses: das Löschen der Beiträge
|
||||||
|
|
|
@ -966,6 +966,9 @@ es-AR:
|
||||||
title: Webhooks
|
title: Webhooks
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: Debido a la falta de actividad reciente por parte de moderadores, los registros en %{instance} fueron cambiados automáticamente para requerir revisión manual, para evitar que %{instance} se use como una plataforma para potenciales malos actores. Podés volver a cambiar esto para abrir los registros en cualquier momento.
|
||||||
|
subject: Los registros de %{instance} se cambiaron automáticamente para requerir aprobación
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: para eliminar sus mensajes
|
delete_statuses: para eliminar sus mensajes
|
||||||
|
|
|
@ -966,6 +966,9 @@ es-MX:
|
||||||
title: Webhooks
|
title: Webhooks
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: Debido a la falta de actividad reciente de los moderadores, se ha cambiado automáticamente el registro en %{instance} para requerir revisión manual, para evitar que la instancia %{instance} sea usada como plataforma para malos actores potenciales. Puedes volver a cambiarlo en cualquier momento para abrir de nuevo los registros.
|
||||||
|
subject: Se ha cambiado automáticamente el registro de %{instance} para requerir aprobación
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: para eliminar sus mensajes
|
delete_statuses: para eliminar sus mensajes
|
||||||
|
|
|
@ -966,6 +966,9 @@ es:
|
||||||
title: Webhooks
|
title: Webhooks
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: Debido a la falta de actividad reciente de los moderadores, se ha cambiado automáticamente el registro en %{instance} para requerir revisión manual, para evitar que la instancia %{instance} sea usada como plataforma para malos actores potenciales. Puedes volver a cambiarlo en cualquier momento para abrir de nuevo los registros.
|
||||||
|
subject: Se ha cambiado automáticamente el registro de %{instance} para requerir aprobación
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: para eliminar sus mensajes
|
delete_statuses: para eliminar sus mensajes
|
||||||
|
|
|
@ -966,6 +966,9 @@ fo:
|
||||||
title: Webhooks/vevhúkar
|
title: Webhooks/vevhúkar
|
||||||
webhook: Webhook/vevhúkur
|
webhook: Webhook/vevhúkur
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: Vegna avmarkað virksemi hjá umsjónarfólki eru skrásetingar á %{instance} broyttar sjálvvirkandi til at krevja manuella eftirkanning fyri at forða at %{instance} verður brúktur sum ein pallur fyri ringar aktørar. Tú kanst skifta aftur til opnar skrásetingar tá tú vilt.
|
||||||
|
subject: Skrásetingar á %{instance} eru sjálvvirkandi broyttar soleiðis at tær krevja váttan
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: at strika teirra postar
|
delete_statuses: at strika teirra postar
|
||||||
|
|
|
@ -966,6 +966,9 @@ hu:
|
||||||
title: Webhookok
|
title: Webhookok
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: A közelmúlt moderátori tevékenységnek hiánya miatt %{instance} regisztráció automatikusan kézi ellenőrzést igénylőre vált, hogy megakadályozz, hogy a %{instance} platformot potenciális rossz szereplők számára használhasson. Bármikor visszakapcsolhatjuk a nyitott regisztrációkhoz.
|
||||||
|
subject: "%{instance} regisztráció automatikusan átállt jóváhagyást igénylőre."
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: bejegyzések törléséről
|
delete_statuses: bejegyzések törléséről
|
||||||
|
|
|
@ -966,6 +966,9 @@ it:
|
||||||
title: Webhook
|
title: Webhook
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: A causa della mancanza di attività recente da parte dei moderatori, le registrazioni su %{instance} sono passate automaticamente alla richiesta di revisione manuale, per evitare che %{instance} venga utilizzata come piattaforma per potenziali malintenzionati. Puoi ripristinarlo per aprire le registrazioni in qualsiasi momento.
|
||||||
|
subject: Le registrazioni per %{instance} sono passate automaticamente alla richiesta di approvazione
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: per cancellare i loro post
|
delete_statuses: per cancellare i loro post
|
||||||
|
|
|
@ -948,6 +948,9 @@ ja:
|
||||||
title: Webhooks
|
title: Webhooks
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: "%{instance} のモデレーターによる活動がしばらくなかったため、%{instance} のアカウント作成は手動での承認を必要とするように自動的に変更されました。これには %{instance} が悪意ある者の踏み台として使われることを防ぐ役割があります。アカウント作成は必要に応じていつでも再び開放できます。"
|
||||||
|
subject: "%{instance} のアカウント作成は自動的に承認制に変更されました"
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: 投稿を削除する
|
delete_statuses: 投稿を削除する
|
||||||
|
|
|
@ -966,6 +966,9 @@ nl:
|
||||||
title: Webhooks
|
title: Webhooks
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: In verband met een gebrek aan recentelijke moderator-activiteit, is de registratie-modus op %{instance} automatisch veranderd naar handmatige beoordeling door moderatoren. Dit om te voorkomen dat %{instance} als platform voor eventueel misbruik kan worden gebruikt. Je kunt op elk gewenst moment veel terugschakelen naar open registraties.
|
||||||
|
subject: De registratie-modus op %{instance} is automatisch veranderd naar handmatige beoordeling door moderatoren
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: het verwijderen van diens berichten
|
delete_statuses: het verwijderen van diens berichten
|
||||||
|
|
|
@ -966,6 +966,9 @@ nn:
|
||||||
title: Webhooker
|
title: Webhooker
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: På grunn av mangel på nyleg moderatoraktivitet, er registreringar på %{instance} automatisk bytt til å krevje manuell gjennomgang, for å hindre at %{instance} vert brukt som ein plattform for potensielle dårlege aktørar. Du kan byte tilbake for å opne registreringar når som helst.
|
||||||
|
subject: Registreringar for %{instance} er automatisk bytt til å krevje godkjenning
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: å slette sine innlegg
|
delete_statuses: å slette sine innlegg
|
||||||
|
|
|
@ -1002,6 +1002,9 @@ pl:
|
||||||
title: Webhooki
|
title: Webhooki
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: Z powodu braku niedawnych działań moderacyjnych, rejestracje na %{instance} wymagają ręcznej weryfikacji (by uniknąć dystrybucji spamu itp.). W dowolnym momencie możesz przywrócić politykę otwartej rejestracji.
|
||||||
|
subject: "%{instance} zostało automatycznie przełączone na zatwierdzanie rejestracji"
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: aby usunąć ich wpisy
|
delete_statuses: aby usunąć ich wpisy
|
||||||
|
|
|
@ -1002,6 +1002,9 @@ sl:
|
||||||
title: Spletne zanke
|
title: Spletne zanke
|
||||||
webhook: Spletna zanka
|
webhook: Spletna zanka
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: Zaradi pomanjkanja moderiranja v zadnjem času, se je za strežnik %{instance} samodejno vklopilo ročno preverjanje in potrjevanje prijav. S tem se prepreči morebitno zlorabo strežnika %{instance}. Prijave lahko kadarkoli spet spremenite nazaj v odprte.
|
||||||
|
subject: Za strežnik %{instance} se je samodejno vklopilo ročno potrjevanje prijav
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: brisanje njihovih objav,
|
delete_statuses: brisanje njihovih objav,
|
||||||
|
|
|
@ -962,6 +962,9 @@ sq:
|
||||||
title: Webhook-ë
|
title: Webhook-ë
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: Për shkak mungese veprimtarie moderatori së fundi, regjistrimet te %{instance} janë kaluar automatikisht të kërkojnë shqyrtim dorazi, për të penguar përdorimin e %{instance} si një platformë për aktorë të këqij. Mund të kaloni kurdo te regjistrime të hapura.
|
||||||
|
subject: Regjistrimet te %{instance} janë kaluar automatikisht të kërkojnë miratim
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: fshirje e postimeve të tij
|
delete_statuses: fshirje e postimeve të tij
|
||||||
|
|
|
@ -1002,6 +1002,9 @@ uk:
|
||||||
title: Вебхуки
|
title: Вебхуки
|
||||||
webhook: Вебхук
|
webhook: Вебхук
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: Через нестачу нещодавньої активності модератора реєстрація на %{instance} автоматично переключена на вимагу ручного відгуку, для запобігання використанню %{instance} як платформи для потенційних поганих гравців. Ви можете будь-коли переключитися на відкриті реєстрації.
|
||||||
|
subject: Реєстрації для %{instance} автоматично перейшли на такі, що вимагають схвалення
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: щоб видалити їхні дописи
|
delete_statuses: щоб видалити їхні дописи
|
||||||
|
|
|
@ -948,6 +948,9 @@ vi:
|
||||||
title: Webhook
|
title: Webhook
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: Do gần đây thiếu kiểm duyệt viên nên việc đăng ký trên %{instance} sẽ tự động chuyển thành duyệt thủ công, để tránh %{instance} bị sử dụng làm nền tảng cho những kẻ xấu. Bạn có thể chuyển nó trở lại trạng thái đăng ký mở bất kỳ lúc nào.
|
||||||
|
subject: Đăng ký mới %{instance} đã được tự động chuyển sang duyệt thủ công
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: xóa tút của họ
|
delete_statuses: xóa tút của họ
|
||||||
|
|
|
@ -948,6 +948,9 @@ zh-CN:
|
||||||
title: Webhooks
|
title: Webhooks
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: 由于近期缺乏管理员活动, %{instance} 上的注册已自动切换为需要手动审核,以防止 %{instance} 被潜在的不良行为者用作平台。您可以随时将其切换回开放注册。
|
||||||
|
subject: "%{instance} 的注册已自动切换为需要批准"
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: 删除其嘟文
|
delete_statuses: 删除其嘟文
|
||||||
|
|
|
@ -950,6 +950,9 @@ zh-TW:
|
||||||
title: Webhooks
|
title: Webhooks
|
||||||
webhook: Webhook
|
webhook: Webhook
|
||||||
admin_mailer:
|
admin_mailer:
|
||||||
|
auto_close_registrations:
|
||||||
|
body: 由於近日缺少管理員活動,%{instance} 上之註冊已自動切換為需要人工審核,以防止 %{instance} 被作為潛在不良行為者之跳板。您隨時能將其切換回開放註冊。
|
||||||
|
subject: "%{instance} 之註冊已自動切換為需要審核"
|
||||||
new_appeal:
|
new_appeal:
|
||||||
actions:
|
actions:
|
||||||
delete_statuses: 要刪除他們的嘟文
|
delete_statuses: 要刪除他們的嘟文
|
||||||
|
|
10
package.json
10
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@mastodon/mastodon",
|
"name": "@mastodon/mastodon",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"packageManager": "yarn@4.0.2",
|
"packageManager": "yarn@4.1.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
"lint:yml": "prettier --check \"**/*.{yaml,yml}\"",
|
"lint:yml": "prettier --check \"**/*.{yaml,yml}\"",
|
||||||
"lint": "yarn lint:js && yarn lint:json && yarn lint:sass && yarn lint:yml",
|
"lint": "yarn lint:js && yarn lint:json && yarn lint:sass && yarn lint:yml",
|
||||||
"postversion": "git push --tags",
|
"postversion": "git push --tags",
|
||||||
"prepare": "husky install",
|
"prepare": "husky",
|
||||||
"start": "node ./streaming/index.js",
|
"start": "node ./streaming/index.js",
|
||||||
"test": "yarn lint && yarn run typecheck && yarn jest",
|
"test": "yarn lint && yarn run typecheck && yarn jest",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
|
@ -176,8 +176,8 @@
|
||||||
"@types/redux-immutable": "^4.0.3",
|
"@types/redux-immutable": "^4.0.3",
|
||||||
"@types/requestidlecallback": "^0.3.5",
|
"@types/requestidlecallback": "^0.3.5",
|
||||||
"@types/webpack": "^4.41.33",
|
"@types/webpack": "^4.41.33",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"@typescript-eslint/parser": "^6.17.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
"babel-jest": "^29.5.0",
|
"babel-jest": "^29.5.0",
|
||||||
"eslint": "^8.41.0",
|
"eslint": "^8.41.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
@ -191,7 +191,7 @@
|
||||||
"eslint-plugin-promise": "~6.1.1",
|
"eslint-plugin-promise": "~6.1.1",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^9.0.11",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"jest-environment-jsdom": "^29.5.0",
|
"jest-environment-jsdom": "^29.5.0",
|
||||||
"lint-staged": "^15.0.0",
|
"lint-staged": "^15.0.0",
|
||||||
|
|
30
spec/services/approve_appeal_service_spec.rb
Normal file
30
spec/services/approve_appeal_service_spec.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ApproveAppealService do
|
||||||
|
describe '#call' do
|
||||||
|
context 'with an existing appeal' do
|
||||||
|
let(:appeal) { Fabricate(:appeal) }
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
|
||||||
|
it 'processes the appeal approval' do
|
||||||
|
expect { subject.call(appeal, account) }
|
||||||
|
.to mark_overruled
|
||||||
|
.and record_approver
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_overruled
|
||||||
|
change(appeal.strike, :overruled_at)
|
||||||
|
.from(nil)
|
||||||
|
.to(be > 1.minute.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_approver
|
||||||
|
change(appeal, :approved_by_account)
|
||||||
|
.from(nil)
|
||||||
|
.to(account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
28
spec/services/remove_domains_from_followers_service_spec.rb
Normal file
28
spec/services/remove_domains_from_followers_service_spec.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe RemoveDomainsFromFollowersService do
|
||||||
|
describe '#call' do
|
||||||
|
context 'with account followers' do
|
||||||
|
let(:account) { Fabricate(:account, domain: nil) }
|
||||||
|
let(:good_domain_account) { Fabricate(:account, domain: 'good.example', protocol: :activitypub) }
|
||||||
|
let(:bad_domain_account) { Fabricate(:account, domain: 'bad.example', protocol: :activitypub) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate :follow, target_account: account, account: good_domain_account
|
||||||
|
Fabricate :follow, target_account: account, account: bad_domain_account
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes followers from supplied domains and sends a notification' do
|
||||||
|
subject.call(account, ['bad.example'])
|
||||||
|
|
||||||
|
expect(account.followers)
|
||||||
|
.to include(good_domain_account)
|
||||||
|
.and not_include(bad_domain_account)
|
||||||
|
expect(ActivityPub::DeliveryWorker)
|
||||||
|
.to have_enqueued_sidekiq_job(anything, account.id, bad_domain_account.inbox_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
37
spec/services/remove_featured_tag_service_spec.rb
Normal file
37
spec/services/remove_featured_tag_service_spec.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe RemoveFeaturedTagService do
|
||||||
|
describe '#call' do
|
||||||
|
context 'with a featured tag' do
|
||||||
|
let(:featured_tag) { Fabricate(:featured_tag) }
|
||||||
|
|
||||||
|
context 'when called by a local account' do
|
||||||
|
let(:account) { Fabricate(:account, domain: nil) }
|
||||||
|
|
||||||
|
it 'destroys the featured tag and sends a distribution' do
|
||||||
|
subject.call(account, featured_tag)
|
||||||
|
|
||||||
|
expect { featured_tag.reload }
|
||||||
|
.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
expect(ActivityPub::AccountRawDistributionWorker)
|
||||||
|
.to have_enqueued_sidekiq_job(anything, account.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when called by a non local account' do
|
||||||
|
let(:account) { Fabricate(:account, domain: 'host.example') }
|
||||||
|
|
||||||
|
it 'destroys the featured tag and does not send a distribution' do
|
||||||
|
subject.call(account, featured_tag)
|
||||||
|
|
||||||
|
expect { featured_tag.reload }
|
||||||
|
.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
expect(ActivityPub::AccountRawDistributionWorker)
|
||||||
|
.to_not have_enqueued_sidekiq_job
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
36
spec/services/unfavourite_service_spec.rb
Normal file
36
spec/services/unfavourite_service_spec.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe UnfavouriteService do
|
||||||
|
describe '#call' do
|
||||||
|
context 'with a favourited status' do
|
||||||
|
let(:status) { Fabricate(:status, account: account) }
|
||||||
|
let!(:favourite) { Fabricate(:favourite, status: status) }
|
||||||
|
|
||||||
|
context 'when the status account is local' do
|
||||||
|
let(:account) { Fabricate(:account, domain: nil) }
|
||||||
|
|
||||||
|
it 'destroys the favourite' do
|
||||||
|
subject.call(favourite.account, status)
|
||||||
|
|
||||||
|
expect { favourite.reload }
|
||||||
|
.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status account is a remote activitypub account' do
|
||||||
|
let(:account) { Fabricate(:account, domain: 'host.example', protocol: :activitypub) }
|
||||||
|
|
||||||
|
it 'destroys the favourite and sends a notification' do
|
||||||
|
subject.call(favourite.account, status)
|
||||||
|
|
||||||
|
expect { favourite.reload }
|
||||||
|
.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
expect(ActivityPub::DeliveryWorker)
|
||||||
|
.to have_enqueued_sidekiq_job(anything, favourite.account.id, status.account.inbox_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
46
spec/services/unmute_service_spec.rb
Normal file
46
spec/services/unmute_service_spec.rb
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe UnmuteService do
|
||||||
|
describe '#call' do
|
||||||
|
let!(:account) { Fabricate(:account) }
|
||||||
|
let!(:target_account) { Fabricate(:account) }
|
||||||
|
|
||||||
|
context 'when account is muting target account' do
|
||||||
|
before { Fabricate :mute, account: account, target_account: target_account }
|
||||||
|
|
||||||
|
context 'when account follows target_account' do
|
||||||
|
before { Fabricate :follow, account: account, target_account: target_account }
|
||||||
|
|
||||||
|
it 'removes the account mute and sets up a merge' do
|
||||||
|
expect { subject.call(account, target_account) }
|
||||||
|
.to remove_account_mute
|
||||||
|
expect(MergeWorker).to have_enqueued_sidekiq_job(target_account.id, account.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when account does not follow target_account' do
|
||||||
|
it 'removes the account mute and does not create a merge' do
|
||||||
|
expect { subject.call(account, target_account) }
|
||||||
|
.to remove_account_mute
|
||||||
|
expect(MergeWorker).to_not have_enqueued_sidekiq_job
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_account_mute
|
||||||
|
change { account.reload.muting?(target_account) }
|
||||||
|
.from(true)
|
||||||
|
.to(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when account is not muting target account' do
|
||||||
|
it 'does nothing and returns' do
|
||||||
|
expect { subject.call(account, target_account) }
|
||||||
|
.to_not(change { account.reload.muting?(target_account) })
|
||||||
|
expect(MergeWorker).to_not have_enqueued_sidekiq_job
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
40
spec/services/vote_service_spec.rb
Normal file
40
spec/services/vote_service_spec.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe VoteService do
|
||||||
|
describe '#call' do
|
||||||
|
subject { described_class.new.call(voter, poll, [0]) }
|
||||||
|
|
||||||
|
context 'with a poll and poll options' do
|
||||||
|
let(:poll) { Fabricate(:poll, account: account, options: %w(Fun UnFun)) }
|
||||||
|
let(:fun_vote) { Fabricate(:poll_vote, poll: poll) }
|
||||||
|
let(:not_fun_vote) { Fabricate(:poll_vote, poll: poll) }
|
||||||
|
let(:voter) { Fabricate(:account, domain: nil) }
|
||||||
|
|
||||||
|
context 'when the poll was created by a local account' do
|
||||||
|
let(:account) { Fabricate(:account, domain: nil) }
|
||||||
|
|
||||||
|
it 'stores the votes and distributes the poll' do
|
||||||
|
expect { subject }
|
||||||
|
.to change(PollVote, :count).by(1)
|
||||||
|
|
||||||
|
expect(ActivityPub::DistributePollUpdateWorker)
|
||||||
|
.to have_enqueued_sidekiq_job(poll.status.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the poll was created by a remote account' do
|
||||||
|
let(:account) { Fabricate(:account, domain: 'host.example') }
|
||||||
|
|
||||||
|
it 'stores the votes and processes delivery' do
|
||||||
|
expect { subject }
|
||||||
|
.to change(PollVote, :count).by(1)
|
||||||
|
|
||||||
|
expect(ActivityPub::DeliveryWorker)
|
||||||
|
.to have_enqueued_sidekiq_job(anything, voter.id, poll.account.inbox_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
30
spec/services/webhook_service_spec.rb
Normal file
30
spec/services/webhook_service_spec.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe WebhookService do
|
||||||
|
describe '#call' do
|
||||||
|
context 'with a relevant event webhook' do
|
||||||
|
let!(:report) { Fabricate(:report) }
|
||||||
|
let!(:webhook) { Fabricate(:webhook, events: ['report.created']) }
|
||||||
|
|
||||||
|
it 'finds and delivers webhook payloads' do
|
||||||
|
expect { subject.call('report.created', report) }
|
||||||
|
.to enqueue_sidekiq_job(Webhooks::DeliveryWorker)
|
||||||
|
.with(
|
||||||
|
webhook.id,
|
||||||
|
anything
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without any relevant event webhooks' do
|
||||||
|
let!(:report) { Fabricate(:report) }
|
||||||
|
|
||||||
|
it 'does not deliver webhook payloads' do
|
||||||
|
expect { subject.call('report.created', report) }
|
||||||
|
.to_not enqueue_sidekiq_job(Webhooks::DeliveryWorker)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
51
streaming/errors.js
Normal file
51
streaming/errors.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typed as a string because otherwise it's a const string, which means we can't
|
||||||
|
* override it in let statements.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
const UNEXPECTED_ERROR_MESSAGE = 'An unexpected error occurred';
|
||||||
|
exports.UNKNOWN_ERROR_MESSAGE = UNEXPECTED_ERROR_MESSAGE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the status and message properties from the error object, if
|
||||||
|
* available for public use. The `unknown` is for catch statements
|
||||||
|
* @param {Error | AuthenticationError | RequestError | unknown} err
|
||||||
|
*/
|
||||||
|
exports.extractStatusAndMessage = function(err) {
|
||||||
|
let statusCode = 500;
|
||||||
|
let errorMessage = UNEXPECTED_ERROR_MESSAGE;
|
||||||
|
if (err instanceof AuthenticationError || err instanceof RequestError) {
|
||||||
|
statusCode = err.status;
|
||||||
|
errorMessage = err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { statusCode, errorMessage };
|
||||||
|
};
|
||||||
|
|
||||||
|
class RequestError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
constructor(message) {
|
||||||
|
super(message);
|
||||||
|
this.name = "RequestError";
|
||||||
|
this.status = 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.RequestError = RequestError;
|
||||||
|
|
||||||
|
class AuthenticationError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
constructor(message) {
|
||||||
|
super(message);
|
||||||
|
this.name = "AuthenticationError";
|
||||||
|
this.status = 401;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.AuthenticationError = AuthenticationError;
|
|
@ -14,6 +14,8 @@ const pg = require('pg');
|
||||||
const dbUrlToConfig = require('pg-connection-string').parse;
|
const dbUrlToConfig = require('pg-connection-string').parse;
|
||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const errors = require('./errors');
|
||||||
|
const { AuthenticationError, RequestError } = require('./errors');
|
||||||
const { logger, httpLogger, initializeLogLevel, attachWebsocketHttpLogger, createWebsocketLogger } = require('./logging');
|
const { logger, httpLogger, initializeLogLevel, attachWebsocketHttpLogger, createWebsocketLogger } = require('./logging');
|
||||||
const { setupMetrics } = require('./metrics');
|
const { setupMetrics } = require('./metrics');
|
||||||
const { isTruthy, normalizeHashtag, firstParam } = require("./utils");
|
const { isTruthy, normalizeHashtag, firstParam } = require("./utils");
|
||||||
|
@ -324,7 +326,7 @@ const startServer = async () => {
|
||||||
// Unfortunately for using the on('upgrade') setup, we need to manually
|
// Unfortunately for using the on('upgrade') setup, we need to manually
|
||||||
// write a HTTP Response to the Socket to close the connection upgrade
|
// write a HTTP Response to the Socket to close the connection upgrade
|
||||||
// attempt, so the following code is to handle all of that.
|
// attempt, so the following code is to handle all of that.
|
||||||
const statusCode = err.status ?? 401;
|
const {statusCode, errorMessage } = errors.extractStatusAndMessage(err);
|
||||||
|
|
||||||
/** @type {Record<string, string | number | import('pino-http').ReqId>} */
|
/** @type {Record<string, string | number | import('pino-http').ReqId>} */
|
||||||
const headers = {
|
const headers = {
|
||||||
|
@ -332,7 +334,7 @@ const startServer = async () => {
|
||||||
'Content-Type': 'text/plain',
|
'Content-Type': 'text/plain',
|
||||||
'Content-Length': 0,
|
'Content-Length': 0,
|
||||||
'X-Request-Id': request.id,
|
'X-Request-Id': request.id,
|
||||||
'X-Error-Message': err.status ? err.toString() : 'An unexpected error occurred'
|
'X-Error-Message': errorMessage
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure the socket is closed once we've finished writing to it:
|
// Ensure the socket is closed once we've finished writing to it:
|
||||||
|
@ -350,7 +352,7 @@ const startServer = async () => {
|
||||||
statusCode,
|
statusCode,
|
||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
}, err.toString());
|
}, errorMessage);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -535,11 +537,7 @@ const startServer = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
if (result.rows.length === 0) {
|
||||||
err = new Error('Invalid access token');
|
reject(new AuthenticationError('Invalid access token'));
|
||||||
// @ts-ignore
|
|
||||||
err.status = 401;
|
|
||||||
|
|
||||||
reject(err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -570,11 +568,7 @@ const startServer = async () => {
|
||||||
const accessToken = location.query.access_token || req.headers['sec-websocket-protocol'];
|
const accessToken = location.query.access_token || req.headers['sec-websocket-protocol'];
|
||||||
|
|
||||||
if (!authorization && !accessToken) {
|
if (!authorization && !accessToken) {
|
||||||
const err = new Error('Missing access token');
|
reject(new AuthenticationError('Missing access token'));
|
||||||
// @ts-ignore
|
|
||||||
err.status = 401;
|
|
||||||
|
|
||||||
reject(err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -651,11 +645,7 @@ const startServer = async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const err = new Error('Access token does not cover required scopes');
|
reject(new AuthenticationError('Access token does not have the required scopes'));
|
||||||
// @ts-ignore
|
|
||||||
err.status = 401;
|
|
||||||
|
|
||||||
reject(err);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -731,11 +721,7 @@ const startServer = async () => {
|
||||||
// If no channelName can be found for the request, then we should terminate
|
// If no channelName can be found for the request, then we should terminate
|
||||||
// the connection, as there's nothing to stream back
|
// the connection, as there's nothing to stream back
|
||||||
if (!channelName) {
|
if (!channelName) {
|
||||||
const err = new Error('Unknown channel requested');
|
next(new RequestError('Unknown channel requested'));
|
||||||
// @ts-ignore
|
|
||||||
err.status = 400;
|
|
||||||
|
|
||||||
next(err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -762,10 +748,7 @@ const startServer = async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasStatusCode = Object.hasOwnProperty.call(err, 'status');
|
const {statusCode, errorMessage } = errors.extractStatusAndMessage(err);
|
||||||
// @ts-ignore
|
|
||||||
const statusCode = hasStatusCode ? err.status : 500;
|
|
||||||
const errorMessage = hasStatusCode ? err.toString() : 'An unexpected error occurred';
|
|
||||||
|
|
||||||
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ error: errorMessage }));
|
res.end(JSON.stringify({ error: errorMessage }));
|
||||||
|
@ -1147,7 +1130,7 @@ const startServer = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {any} res
|
* @param {http.ServerResponse} res
|
||||||
*/
|
*/
|
||||||
const httpNotFound = res => {
|
const httpNotFound = res => {
|
||||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
@ -1162,16 +1145,29 @@ const startServer = async () => {
|
||||||
api.use(errorMiddleware);
|
api.use(errorMiddleware);
|
||||||
|
|
||||||
api.get('/api/v1/streaming/*', (req, res) => {
|
api.get('/api/v1/streaming/*', (req, res) => {
|
||||||
// @ts-ignore
|
const channelName = channelNameFromPath(req);
|
||||||
channelNameToIds(req, channelNameFromPath(req), req.query).then(({ channelIds, options }) => {
|
|
||||||
|
// FIXME: In theory we'd never actually reach here due to
|
||||||
|
// authenticationMiddleware catching this case, however, we need to refactor
|
||||||
|
// how those middlewares work, so I'm adding the extra check in here.
|
||||||
|
if (!channelName) {
|
||||||
|
httpNotFound(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
channelNameToIds(req, channelName, req.query).then(({ channelIds, options }) => {
|
||||||
const onSend = streamToHttp(req, res);
|
const onSend = streamToHttp(req, res);
|
||||||
const onEnd = streamHttpEnd(req, subscriptionHeartbeat(channelIds));
|
const onEnd = streamHttpEnd(req, subscriptionHeartbeat(channelIds));
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
streamFrom(channelIds, req, req.log, onSend, onEnd, 'eventsource', options.needsFiltering, options.allowLocalOnly);
|
streamFrom(channelIds, req, req.log, onSend, onEnd, 'eventsource', options.needsFiltering, options.allowLocalOnly);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
res.log.info({ err }, 'Subscription error:', err.toString());
|
const {statusCode, errorMessage } = errors.extractStatusAndMessage(err);
|
||||||
httpNotFound(res);
|
|
||||||
|
res.log.info({ err }, 'Eventsource subscription error');
|
||||||
|
|
||||||
|
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: errorMessage }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1286,8 +1282,8 @@ const startServer = async () => {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'hashtag':
|
case 'hashtag':
|
||||||
if (!params.tag || params.tag.length === 0) {
|
if (!params.tag) {
|
||||||
reject('No tag for stream provided');
|
reject(new RequestError('Missing tag name parameter'));
|
||||||
} else {
|
} else {
|
||||||
resolve({
|
resolve({
|
||||||
channelIds: [`timeline:hashtag:${normalizeHashtag(params.tag)}`],
|
channelIds: [`timeline:hashtag:${normalizeHashtag(params.tag)}`],
|
||||||
|
@ -1297,8 +1293,8 @@ const startServer = async () => {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'hashtag:local':
|
case 'hashtag:local':
|
||||||
if (!params.tag || params.tag.length === 0) {
|
if (!params.tag) {
|
||||||
reject('No tag for stream provided');
|
reject(new RequestError('Missing tag name parameter'));
|
||||||
} else {
|
} else {
|
||||||
resolve({
|
resolve({
|
||||||
channelIds: [`timeline:hashtag:${normalizeHashtag(params.tag)}:local`],
|
channelIds: [`timeline:hashtag:${normalizeHashtag(params.tag)}:local`],
|
||||||
|
@ -1308,19 +1304,23 @@ const startServer = async () => {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'list':
|
case 'list':
|
||||||
// @ts-ignore
|
if (!params.list) {
|
||||||
|
reject(new RequestError('Missing list name parameter'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
authorizeListAccess(params.list, req).then(() => {
|
authorizeListAccess(params.list, req).then(() => {
|
||||||
resolve({
|
resolve({
|
||||||
channelIds: [`timeline:list:${params.list}`],
|
channelIds: [`timeline:list:${params.list}`],
|
||||||
options: { needsFiltering: false, allowLocalOnly: true },
|
options: { needsFiltering: false, allowLocalOnly: true },
|
||||||
});
|
});
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
reject('Not authorized to stream this list');
|
reject(new AuthenticationError('Not authorized to stream this list'));
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
reject('Unknown stream type');
|
reject(new RequestError('Unknown stream type'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1374,8 +1374,17 @@ const startServer = async () => {
|
||||||
stopHeartbeat,
|
stopHeartbeat,
|
||||||
};
|
};
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
logger.error({ err }, 'Subscription error');
|
const {statusCode, errorMessage } = errors.extractStatusAndMessage(err);
|
||||||
websocket.send(JSON.stringify({ error: err.toString() }));
|
|
||||||
|
logger.error({ err }, 'Websocket subscription error');
|
||||||
|
|
||||||
|
// If we have a socket that is alive and open still, send the error back to the client:
|
||||||
|
if (websocket.isAlive && websocket.readyState === websocket.OPEN) {
|
||||||
|
websocket.send(JSON.stringify({
|
||||||
|
error: errorMessage,
|
||||||
|
status: statusCode
|
||||||
|
}));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1414,10 +1423,11 @@ const startServer = async () => {
|
||||||
channelNameToIds(request, channelName, params).then(({ channelIds }) => {
|
channelNameToIds(request, channelName, params).then(({ channelIds }) => {
|
||||||
removeSubscription(session, channelIds);
|
removeSubscription(session, channelIds);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
logger.error({err}, 'Unsubscribe error');
|
logger.error({err}, 'Websocket unsubscribe error');
|
||||||
|
|
||||||
// If we have a socket that is alive and open still, send the error back to the client:
|
// If we have a socket that is alive and open still, send the error back to the client:
|
||||||
if (websocket.isAlive && websocket.readyState === websocket.OPEN) {
|
if (websocket.isAlive && websocket.readyState === websocket.OPEN) {
|
||||||
|
// TODO: Use a better error response here
|
||||||
websocket.send(JSON.stringify({ error: "Error unsubscribing from channel" }));
|
websocket.send(JSON.stringify({ error: "Error unsubscribing from channel" }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@mastodon/streaming",
|
"name": "@mastodon/streaming",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"packageManager": "yarn@4.0.2",
|
"packageManager": "yarn@4.1.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue