mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 11:11:11 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd493378dc | ||
|
|
d609819aa7 | ||
|
|
4f6a53c22c | ||
|
|
ab872f28b9 | ||
|
|
1103ebdc55 | ||
|
|
93eb2ac28a | ||
|
|
04e0b85f5b | ||
|
|
4046affea9 | ||
|
|
96a96a79ca | ||
|
|
aec9ccba3d | ||
|
|
9c927683db | ||
|
|
fbbf8b9a8c | ||
|
|
b7e34ade1d | ||
|
|
e68754d2a2 | ||
|
|
31316aa082 | ||
|
|
27c1e13aa8 | ||
|
|
17c04fe04b | ||
|
|
ffddcc7c1d |
20
CHANGELOG.md
20
CHANGELOG.md
@@ -2,6 +2,26 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.5.7] - 2026-02-24
|
||||
|
||||
### Security
|
||||
|
||||
- Reject unconfirmed FASPs (#37926 by @oneiros, [GHSA-qgmm-vr4c-ggjg](https://github.com/mastodon/mastodon/security/advisories/GHSA-qgmm-vr4c-ggjg))
|
||||
- Re-use custom socket class for FASP requests (#37925 by @oneiros, [GHSA-46w6-g98f-wxqm](https://github.com/mastodon/mastodon/security/advisories/GHSA-46w6-g98f-wxqm))
|
||||
|
||||
### Added
|
||||
|
||||
- Add `--suspended-only` option to `tootctl emoji purge` (#37828 and #37861 by @ClearlyClaire and @mjankowski)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix emoji data not being properly cached (#37858 by @ChaosExAnima)
|
||||
- Fix delete & redraft of pending posts (#37839 by @ClearlyClaire)
|
||||
- Fix processing separate key documents without the ActivityStreams context (#37826 by @ClearlyClaire)
|
||||
- Fix custom emojis not being purged on domain suspension (#37808 by @ClearlyClaire)
|
||||
- Fix users without special permissions being able to stream disabled timelines (#37791 by @ClearlyClaire)
|
||||
- Fix processing of object updates with duplicate hashtags (#37756 by @ClearlyClaire)
|
||||
|
||||
## [4.5.6] - 2026-02-03
|
||||
|
||||
### Security
|
||||
|
||||
@@ -47,7 +47,7 @@ class Api::Fasp::BaseController < ApplicationController
|
||||
provider = nil
|
||||
|
||||
Linzer.verify!(request.rack_request, no_older_than: 5.minutes) do |keyid|
|
||||
provider = Fasp::Provider.find(keyid)
|
||||
provider = Fasp::Provider.confirmed.find(keyid)
|
||||
Linzer.new_ed25519_public_key(provider.provider_public_key_pem, keyid)
|
||||
end
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
application: doorkeeper_token.application,
|
||||
poll: status_params[:poll],
|
||||
content_type: status_params[:content_type],
|
||||
local_only: status_params[:local_only],
|
||||
allowed_mentions: status_params[:allowed_mentions],
|
||||
idempotency: request.headers['Idempotency-Key'],
|
||||
with_rate_limit: true
|
||||
@@ -191,6 +192,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
:language,
|
||||
:scheduled_at,
|
||||
:content_type,
|
||||
:local_only,
|
||||
allowed_mentions: [],
|
||||
media_ids: [],
|
||||
media_attributes: [
|
||||
|
||||
@@ -70,6 +70,10 @@ module JsonLdHelper
|
||||
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
|
||||
end
|
||||
|
||||
def supported_security_context?(json)
|
||||
!json.nil? && equals_or_includes?(json['@context'], 'https://w3id.org/security/v1')
|
||||
end
|
||||
|
||||
def unsupported_uri_scheme?(uri)
|
||||
uri.nil? || !uri.start_with?('http://', 'https://')
|
||||
end
|
||||
|
||||
@@ -228,10 +228,6 @@ export function submitCompose(overridePrivacy = null, successCallback = undefine
|
||||
return;
|
||||
}
|
||||
|
||||
if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
|
||||
status = status + ' 👁️';
|
||||
}
|
||||
|
||||
dispatch(submitComposeRequest());
|
||||
|
||||
// If we're editing a post with media attachments, those have not
|
||||
@@ -262,6 +258,7 @@ export function submitCompose(overridePrivacy = null, successCallback = undefine
|
||||
status,
|
||||
spoiler_text,
|
||||
content_type: getState().getIn(['compose', 'content_type']),
|
||||
local_only: getState().getIn(['compose', 'advanced_options', 'do_not_federate']),
|
||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||
media_ids: media.map(item => item.get('id')),
|
||||
media_attributes,
|
||||
|
||||
@@ -109,7 +109,7 @@ export function fetchStatusFail(id, error, skipLoading, parentQuotePostId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function redraft(status, raw_text, content_type) {
|
||||
export function redraft(status, raw_text, content_type, quoted_status_id = null) {
|
||||
return (dispatch, getState) => {
|
||||
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||
|
||||
@@ -117,6 +117,7 @@ export function redraft(status, raw_text, content_type) {
|
||||
type: REDRAFT,
|
||||
status,
|
||||
raw_text,
|
||||
quoted_status_id,
|
||||
content_type,
|
||||
maxOptions,
|
||||
});
|
||||
@@ -135,7 +136,7 @@ export const editStatus = (id) => (dispatch, getState) => {
|
||||
api().get(`/api/v1/statuses/${id}/source`).then(response => {
|
||||
dispatch(fetchStatusSourceSuccess());
|
||||
ensureComposeIsVisible(getState);
|
||||
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type));
|
||||
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type, response.data.quote?.quoted_status?.id));
|
||||
}).catch(error => {
|
||||
dispatch(fetchStatusSourceFail(error));
|
||||
});
|
||||
|
||||
@@ -21,10 +21,19 @@ export async function importEmojiData(localeString: string, path?: string) {
|
||||
path ??= await localeToPath(locale);
|
||||
}
|
||||
|
||||
// Fix from #37858. Check if we've loaded this path before.
|
||||
const existing = await loadLatestEtag(locale);
|
||||
if (existing === path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale, path);
|
||||
if (!emojis) {
|
||||
return;
|
||||
}
|
||||
|
||||
await putLatestEtag(path, locale); // Fix from #37858. Put the path as the ETag to ensure we don't load the same data again.
|
||||
|
||||
const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis);
|
||||
await putEmojiData(flattenedEmojis, locale);
|
||||
return flattenedEmojis;
|
||||
|
||||
@@ -630,7 +630,6 @@ export const composeReducer = (state = initialState, action) => {
|
||||
case REDRAFT: {
|
||||
const do_not_federate = !!action.status.get('local_only');
|
||||
let text = action.raw_text || unescapeHTML(expandMentions(action.status));
|
||||
if (do_not_federate) text = text.replace(/ ?👁\ufe0f?\u200b?$/, '');
|
||||
return state.withMutations(map => {
|
||||
map.set('text', text);
|
||||
map.set('content_type', action.content_type || 'text/plain');
|
||||
@@ -647,7 +646,7 @@ export const composeReducer = (state = initialState, action) => {
|
||||
map => map.merge(new ImmutableMap({ do_not_federate })),
|
||||
);
|
||||
map.set('id', null);
|
||||
map.set('quoted_status_id', action.status.getIn(['quote', 'quoted_status'], null));
|
||||
map.set('quoted_status_id', action.quoted_status_id);
|
||||
// Mastodon-authored posts can be expected to have at most one automatic approval policy
|
||||
map.set('quote_policy', action.status.getIn(['quote_approval', 'automatic', 0]) || 'nobody');
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export function fetchStatusFail(id, error, skipLoading, parentQuotePostId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function redraft(status, raw_text) {
|
||||
export function redraft(status, raw_text, quoted_status_id = null) {
|
||||
return (dispatch, getState) => {
|
||||
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||
|
||||
@@ -117,6 +117,7 @@ export function redraft(status, raw_text) {
|
||||
type: REDRAFT,
|
||||
status,
|
||||
raw_text,
|
||||
quoted_status_id,
|
||||
maxOptions,
|
||||
});
|
||||
};
|
||||
@@ -169,7 +170,7 @@ export function deleteStatus(id, withRedraft = false) {
|
||||
dispatch(importFetchedAccount(response.data.account));
|
||||
|
||||
if (withRedraft) {
|
||||
dispatch(redraft(status, response.data.text));
|
||||
dispatch(redraft(status, response.data.text, response.data.quote?.quoted_status?.id));
|
||||
ensureComposeIsVisible(getState);
|
||||
} else {
|
||||
dispatch(showAlert({ message: messages.deleteSuccess }));
|
||||
|
||||
@@ -21,10 +21,19 @@ export async function importEmojiData(localeString: string, path?: string) {
|
||||
path ??= await localeToPath(locale);
|
||||
}
|
||||
|
||||
// Fix from #37858. Check if we've loaded this path before.
|
||||
const existing = await loadLatestEtag(locale);
|
||||
if (existing === path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale, path);
|
||||
if (!emojis) {
|
||||
return;
|
||||
}
|
||||
|
||||
await putLatestEtag(path, locale); // Fix from #37858. Put the path as the ETag to ensure we don't load the same data again.
|
||||
|
||||
const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis);
|
||||
await putEmojiData(flattenedEmojis, locale);
|
||||
return flattenedEmojis;
|
||||
|
||||
@@ -530,7 +530,7 @@ export const composeReducer = (state = initialState, action) => {
|
||||
map.set('sensitive', action.status.get('sensitive'));
|
||||
map.set('language', action.status.get('language'));
|
||||
map.set('id', null);
|
||||
map.set('quoted_status_id', action.status.getIn(['quote', 'quoted_status'], null));
|
||||
map.set('quoted_status_id', action.quoted_status_id);
|
||||
// Mastodon-authored posts can be expected to have at most one automatic approval policy
|
||||
map.set('quote_policy', action.status.getIn(['quote_approval', 'automatic', 0]) || 'nobody');
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class Fasp::Request
|
||||
response = HTTP
|
||||
.headers(headers)
|
||||
.use(http_signature: { key:, covered_components: COVERED_COMPONENTS })
|
||||
.send(verb, url, body:)
|
||||
.send(verb, url, body:, socket_class: ::Request::Socket)
|
||||
|
||||
validate!(response)
|
||||
@provider.delivery_failure_tracker.track_success!
|
||||
|
||||
@@ -349,5 +349,5 @@ class Request
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :ClientLimit, :Socket, :ProxySocket
|
||||
private_constant :ClientLimit
|
||||
end
|
||||
|
||||
@@ -37,6 +37,7 @@ class Fasp::Provider < ApplicationRecord
|
||||
before_create :create_keypair
|
||||
after_commit :update_remote_capabilities
|
||||
|
||||
scope :confirmed, -> { where(confirmed: true) }
|
||||
scope :with_capability, lambda { |capability_name|
|
||||
where('fasp_providers.capabilities @> ?::jsonb', "[{\"id\": \"#{capability_name}\", \"enabled\": true}]")
|
||||
}
|
||||
|
||||
@@ -495,7 +495,13 @@ class Status < ApplicationRecord
|
||||
def set_local_only
|
||||
return unless account.domain.nil? && !attribute_changed?(:local_only)
|
||||
|
||||
self.local_only = marked_local_only?
|
||||
self.local_only = true if thread&.local_only? && local_only.nil?
|
||||
|
||||
if reblog?
|
||||
self.local_only = reblog.local_only
|
||||
elsif local_only.nil?
|
||||
self.local_only = marked_local_only?
|
||||
end
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
|
||||
@@ -13,7 +13,7 @@ class REST::BaseQuoteSerializer < ActiveModel::Serializer
|
||||
end
|
||||
|
||||
def quoted_status
|
||||
object.quoted_status if object.accepted? && object.quoted_status.present? && !object.quoted_status&.reblog? && status_filter.filter_state_for_quote != 'unauthorized'
|
||||
object.quoted_status if (object.accepted? || instance_options[:source_requested]) && object.quoted_status.present? && !object.quoted_status&.reblog? && status_filter.filter_state_for_quote != 'unauthorized'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -12,7 +12,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
|
||||
@json = fetch_resource(uri, false)
|
||||
|
||||
raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil?
|
||||
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json)
|
||||
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json) || (supported_security_context?(@json) && @json['owner'].present? && !actor_type?)
|
||||
raise Error, "Unexpected object type for key #{uri}" unless expected_type?
|
||||
return find_actor(@json['id'], @json, suppress_errors) if actor_type?
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
Tag.find_or_create_by_names([tag]).filter(&:valid?)
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
[]
|
||||
end
|
||||
end.uniq
|
||||
|
||||
return unless @status.distributable?
|
||||
|
||||
|
||||
@@ -29,7 +29,12 @@ class BlockDomainService < BaseService
|
||||
suspend_accounts!
|
||||
end
|
||||
|
||||
DomainClearMediaWorker.perform_async(domain_block.id) if domain_block.reject_media?
|
||||
if domain_block.suspend?
|
||||
# Account images and attachments are already handled by `suspend_accounts!`
|
||||
PurgeCustomEmojiWorker.perform_async(blocked_domain)
|
||||
elsif domain_block.reject_media?
|
||||
DomainClearMediaWorker.perform_async(domain_block.id)
|
||||
end
|
||||
end
|
||||
|
||||
def silence_accounts!
|
||||
|
||||
@@ -247,6 +247,7 @@ class PostStatusService < BaseService
|
||||
language: valid_locale_cascade(@options[:language], @account.user&.preferred_posting_language, I18n.default_locale),
|
||||
application: @options[:application],
|
||||
content_type: @options[:content_type] || @account.user&.setting_default_content_type,
|
||||
local_only: @options[:local_only],
|
||||
rate_limit: @options[:with_rate_limit],
|
||||
quote_approval_policy: @options[:quote_approval_policy],
|
||||
}.compact
|
||||
|
||||
@@ -8,7 +8,7 @@ class Fasp::BaseWorker
|
||||
private
|
||||
|
||||
def with_provider(provider)
|
||||
return unless provider.available?
|
||||
return unless provider.confirmed? && provider.available?
|
||||
|
||||
yield
|
||||
rescue *Mastodon::HTTP_CONNECTION_ERRORS
|
||||
|
||||
15
app/workers/purge_custom_emoji_worker.rb
Normal file
15
app/workers/purge_custom_emoji_worker.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PurgeCustomEmojiWorker
|
||||
include Sidekiq::IterableJob
|
||||
|
||||
def build_enumerator(domain, cursor:)
|
||||
return if domain.blank?
|
||||
|
||||
active_record_batches_enumerator(CustomEmoji.by_domain_and_subdomains(domain), cursor:)
|
||||
end
|
||||
|
||||
def each_iteration(custom_emojis, _domain)
|
||||
AttachmentBatch.new(CustomEmoji, custom_emojis).delete
|
||||
end
|
||||
end
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
web:
|
||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||
# build: .
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.5.6
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.5.7
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec puma -C config/puma.rb
|
||||
@@ -83,7 +83,7 @@ services:
|
||||
# build:
|
||||
# dockerfile: ./streaming/Dockerfile
|
||||
# context: .
|
||||
image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.6
|
||||
image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.7
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: node ./streaming/index.js
|
||||
@@ -102,7 +102,7 @@ services:
|
||||
sidekiq:
|
||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||
# build: .
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.5.6
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.5.7
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec sidekiq
|
||||
|
||||
@@ -109,15 +109,27 @@ module Mastodon::CLI
|
||||
end
|
||||
|
||||
option :remote_only, type: :boolean
|
||||
option :suspended_only, type: :boolean
|
||||
desc 'purge', 'Remove all custom emoji'
|
||||
long_desc <<-LONG_DESC
|
||||
Removes all custom emoji.
|
||||
|
||||
With the --remote-only option, only remote emoji will be deleted.
|
||||
|
||||
With the --suspended-only option, only emoji from suspended servers will be deleted.
|
||||
LONG_DESC
|
||||
def purge
|
||||
scope = options[:remote_only] ? CustomEmoji.remote : CustomEmoji
|
||||
scope.in_batches.destroy_all
|
||||
if options[:suspended_only]
|
||||
DomainBlock.where(severity: :suspend).find_each do |domain_block|
|
||||
CustomEmoji.by_domain_and_subdomains(domain_block.domain).find_in_batches do |custom_emojis|
|
||||
AttachmentBatch.new(CustomEmoji, custom_emojis).delete
|
||||
end
|
||||
end
|
||||
else
|
||||
scope = options[:remote_only] ? CustomEmoji.remote : CustomEmoji
|
||||
scope.in_batches.destroy_all
|
||||
end
|
||||
|
||||
say('OK', :green)
|
||||
end
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ module Mastodon
|
||||
end
|
||||
|
||||
def patch
|
||||
6
|
||||
7
|
||||
end
|
||||
|
||||
def default_prerelease
|
||||
@@ -46,6 +46,7 @@ module Mastodon
|
||||
def api_versions
|
||||
{
|
||||
mastodon: 7,
|
||||
glitch: 1,
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -78,6 +78,24 @@ RSpec.describe Fasp::Request do
|
||||
expect(provider.delivery_failure_tracker.failures).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the provider host name resolves to a private address' do
|
||||
around do |example|
|
||||
WebMock.disable!
|
||||
example.run
|
||||
WebMock.enable!
|
||||
end
|
||||
|
||||
it 'raises Mastodon::ValidationError' do
|
||||
resolver = instance_double(Resolv::DNS)
|
||||
|
||||
allow(resolver).to receive(:getaddresses).with('reqprov.example.com').and_return(%w(0.0.0.0 2001:db8::face))
|
||||
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||
|
||||
expect { subject.send(method, '/test_path') }.to raise_error(Mastodon::ValidationError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get' do
|
||||
|
||||
@@ -23,6 +23,36 @@ RSpec.describe Mastodon::CLI::Emoji do
|
||||
.to output_results('OK')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with --suspended-only and existing custom emoji on blocked servers' do
|
||||
let(:blocked_domain) { 'evil.com' }
|
||||
let(:blocked_subdomain) { 'subdomain.evil.org' }
|
||||
let(:blocked_domain_without_emoji) { 'blocked.com' }
|
||||
let(:silenced_domain) { 'silenced.com' }
|
||||
|
||||
let(:options) { { suspended_only: true } }
|
||||
|
||||
before do
|
||||
Fabricate(:custom_emoji)
|
||||
Fabricate(:custom_emoji, domain: blocked_domain)
|
||||
Fabricate(:custom_emoji, domain: blocked_subdomain)
|
||||
Fabricate(:custom_emoji, domain: silenced_domain)
|
||||
|
||||
Fabricate(:domain_block, severity: :suspend, domain: blocked_domain)
|
||||
Fabricate(:domain_block, severity: :suspend, domain: 'evil.org')
|
||||
Fabricate(:domain_block, severity: :suspend, domain: blocked_domain_without_emoji)
|
||||
Fabricate(:domain_block, severity: :silence, domain: silenced_domain)
|
||||
end
|
||||
|
||||
it 'reports a successful purge' do
|
||||
expect { subject }
|
||||
.to output_results('OK')
|
||||
.and change { CustomEmoji.by_domain_and_subdomains(blocked_domain).count }.to(0)
|
||||
.and change { CustomEmoji.by_domain_and_subdomains('evil.org').count }.to(0)
|
||||
.and not_change { CustomEmoji.by_domain_and_subdomains(silenced_domain).count }
|
||||
.and(not_change { CustomEmoji.local.count })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#import' do
|
||||
|
||||
@@ -6,34 +6,33 @@ RSpec.describe 'Api::Fasp::DataSharing::V0::BackfillRequests', feature: :fasp do
|
||||
include ProviderRequestHelper
|
||||
|
||||
describe 'POST /api/fasp/data_sharing/v0/backfill_requests' do
|
||||
let(:provider) { Fabricate(:fasp_provider) }
|
||||
subject do
|
||||
post api_fasp_data_sharing_v0_backfill_requests_path, headers:, params:, as: :json
|
||||
end
|
||||
|
||||
let(:provider) { Fabricate(:confirmed_fasp) }
|
||||
let(:params) { { category: 'content', maxCount: 10 } }
|
||||
let(:headers) do
|
||||
request_authentication_headers(provider,
|
||||
url: api_fasp_data_sharing_v0_backfill_requests_url,
|
||||
method: :post,
|
||||
body: params)
|
||||
end
|
||||
|
||||
it_behaves_like 'forbidden for unconfirmed provider'
|
||||
|
||||
context 'with valid parameters' do
|
||||
it 'creates a new backfill request' do
|
||||
params = { category: 'content', maxCount: 10 }
|
||||
headers = request_authentication_headers(provider,
|
||||
url: api_fasp_data_sharing_v0_backfill_requests_url,
|
||||
method: :post,
|
||||
body: params)
|
||||
|
||||
expect do
|
||||
post api_fasp_data_sharing_v0_backfill_requests_path, headers:, params:, as: :json
|
||||
end.to change(Fasp::BackfillRequest, :count).by(1)
|
||||
expect { subject }.to change(Fasp::BackfillRequest, :count).by(1)
|
||||
expect(response).to have_http_status(201)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid parameters' do
|
||||
it 'does not create a backfill request' do
|
||||
params = { category: 'unknown', maxCount: 10 }
|
||||
headers = request_authentication_headers(provider,
|
||||
url: api_fasp_data_sharing_v0_backfill_requests_url,
|
||||
method: :post,
|
||||
body: params)
|
||||
let(:params) { { category: 'unknown', maxCount: 10 } }
|
||||
|
||||
expect do
|
||||
post api_fasp_data_sharing_v0_backfill_requests_path, headers:, params:, as: :json
|
||||
end.to_not change(Fasp::BackfillRequest, :count)
|
||||
it 'does not create a backfill request' do
|
||||
expect { subject }.to_not change(Fasp::BackfillRequest, :count)
|
||||
expect(response).to have_http_status(422)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,15 +6,22 @@ RSpec.describe 'Api::Fasp::DataSharing::V0::Continuations', feature: :fasp do
|
||||
include ProviderRequestHelper
|
||||
|
||||
describe 'POST /api/fasp/data_sharing/v0/backfill_requests/:id/continuations' do
|
||||
let(:backfill_request) { Fabricate(:fasp_backfill_request) }
|
||||
let(:provider) { backfill_request.fasp_provider }
|
||||
subject do
|
||||
post api_fasp_data_sharing_v0_backfill_request_continuation_path(backfill_request), headers:, as: :json
|
||||
end
|
||||
|
||||
let(:provider) { Fabricate(:confirmed_fasp) }
|
||||
let(:backfill_request) { Fabricate(:fasp_backfill_request, fasp_provider: provider) }
|
||||
let(:headers) do
|
||||
request_authentication_headers(provider,
|
||||
url: api_fasp_data_sharing_v0_backfill_request_continuation_url(backfill_request),
|
||||
method: :post)
|
||||
end
|
||||
|
||||
it_behaves_like 'forbidden for unconfirmed provider'
|
||||
|
||||
it 'queues a job to continue the given backfill request' do
|
||||
headers = request_authentication_headers(provider,
|
||||
url: api_fasp_data_sharing_v0_backfill_request_continuation_url(backfill_request),
|
||||
method: :post)
|
||||
|
||||
post api_fasp_data_sharing_v0_backfill_request_continuation_path(backfill_request), headers:, as: :json
|
||||
subject
|
||||
expect(response).to have_http_status(204)
|
||||
expect(Fasp::BackfillWorker).to have_enqueued_sidekiq_job(backfill_request.id)
|
||||
end
|
||||
|
||||
@@ -6,51 +6,57 @@ RSpec.describe 'Api::Fasp::DataSharing::V0::EventSubscriptions', feature: :fasp
|
||||
include ProviderRequestHelper
|
||||
|
||||
describe 'POST /api/fasp/data_sharing/v0/event_subscriptions' do
|
||||
let(:provider) { Fabricate(:fasp_provider) }
|
||||
subject do
|
||||
post api_fasp_data_sharing_v0_event_subscriptions_path, headers:, params:, as: :json
|
||||
end
|
||||
|
||||
let(:provider) { Fabricate(:confirmed_fasp) }
|
||||
let(:params) { { category: 'content', subscriptionType: 'lifecycle', maxBatchSize: 10 } }
|
||||
let(:headers) do
|
||||
request_authentication_headers(provider,
|
||||
url: api_fasp_data_sharing_v0_event_subscriptions_url,
|
||||
method: :post,
|
||||
body: params)
|
||||
end
|
||||
|
||||
it_behaves_like 'forbidden for unconfirmed provider'
|
||||
|
||||
context 'with valid parameters' do
|
||||
it 'creates a new subscription' do
|
||||
params = { category: 'content', subscriptionType: 'lifecycle', maxBatchSize: 10 }
|
||||
headers = request_authentication_headers(provider,
|
||||
url: api_fasp_data_sharing_v0_event_subscriptions_url,
|
||||
method: :post,
|
||||
body: params)
|
||||
|
||||
expect do
|
||||
post api_fasp_data_sharing_v0_event_subscriptions_path, headers:, params:, as: :json
|
||||
subject
|
||||
end.to change(Fasp::Subscription, :count).by(1)
|
||||
expect(response).to have_http_status(201)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid parameters' do
|
||||
it 'does not create a subscription' do
|
||||
params = { category: 'unknown' }
|
||||
headers = request_authentication_headers(provider,
|
||||
url: api_fasp_data_sharing_v0_event_subscriptions_url,
|
||||
method: :post,
|
||||
body: params)
|
||||
let(:params) { { category: 'unknown' } }
|
||||
|
||||
expect do
|
||||
post api_fasp_data_sharing_v0_event_subscriptions_path, headers:, params:, as: :json
|
||||
end.to_not change(Fasp::Subscription, :count)
|
||||
it 'does not create a subscription' do
|
||||
expect { subject }.to_not change(Fasp::Subscription, :count)
|
||||
expect(response).to have_http_status(422)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/fasp/data_sharing/v0/event_subscriptions/:id' do
|
||||
let(:subscription) { Fabricate(:fasp_subscription) }
|
||||
let(:provider) { subscription.fasp_provider }
|
||||
subject do
|
||||
delete api_fasp_data_sharing_v0_event_subscription_path(subscription), headers:, as: :json
|
||||
end
|
||||
|
||||
let(:provider) { Fabricate(:confirmed_fasp) }
|
||||
let!(:subscription) { Fabricate(:fasp_subscription, fasp_provider: provider) }
|
||||
let(:headers) do
|
||||
request_authentication_headers(provider,
|
||||
url: api_fasp_data_sharing_v0_event_subscription_url(subscription),
|
||||
method: :delete)
|
||||
end
|
||||
|
||||
it_behaves_like 'forbidden for unconfirmed provider'
|
||||
|
||||
it 'deletes the subscription' do
|
||||
headers = request_authentication_headers(provider,
|
||||
url: api_fasp_data_sharing_v0_event_subscription_url(subscription),
|
||||
method: :delete)
|
||||
|
||||
expect do
|
||||
delete api_fasp_data_sharing_v0_event_subscription_path(subscription), headers:, as: :json
|
||||
end.to change(Fasp::Subscription, :count).by(-1)
|
||||
expect { subject }.to change(Fasp::Subscription, :count).by(-1)
|
||||
expect(response).to have_http_status(204)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,18 +6,23 @@ RSpec.describe 'Api::Fasp::Debug::V0::Callback::Responses', feature: :fasp do
|
||||
include ProviderRequestHelper
|
||||
|
||||
describe 'POST /api/fasp/debug/v0/callback/responses' do
|
||||
let(:provider) { Fabricate(:debug_fasp) }
|
||||
subject do
|
||||
post api_fasp_debug_v0_callback_responses_path, headers:, params: payload, as: :json
|
||||
end
|
||||
|
||||
let(:provider) { Fabricate(:confirmed_fasp) }
|
||||
let(:payload) { { test: 'call' } }
|
||||
let(:headers) do
|
||||
request_authentication_headers(provider,
|
||||
url: api_fasp_debug_v0_callback_responses_url,
|
||||
method: :post,
|
||||
body: payload)
|
||||
end
|
||||
|
||||
it_behaves_like 'forbidden for unconfirmed provider'
|
||||
|
||||
it 'create a record of the callback' do
|
||||
payload = { test: 'call' }
|
||||
headers = request_authentication_headers(provider,
|
||||
url: api_fasp_debug_v0_callback_responses_url,
|
||||
method: :post,
|
||||
body: payload)
|
||||
|
||||
expect do
|
||||
post api_fasp_debug_v0_callback_responses_path, headers:, params: payload, as: :json
|
||||
end.to change(Fasp::DebugCallback, :count).by(1)
|
||||
expect { subject }.to change(Fasp::DebugCallback, :count).by(1)
|
||||
expect(response).to have_http_status(201)
|
||||
|
||||
debug_callback = Fasp::DebugCallback.last
|
||||
|
||||
@@ -437,6 +437,63 @@ RSpec.describe '/api/v1/statuses' do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with local_only param set to true' do
|
||||
let(:params) { { status: 'Hello world', local_only: true } }
|
||||
|
||||
it 'returns a local-only post' do
|
||||
subject
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
expect(response.parsed_body[:content]).to match(/Hello world/)
|
||||
expect(response.parsed_body[:local_only]).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'with local_only param set to false' do
|
||||
let(:params) { { status: 'Hello world', local_only: false } }
|
||||
|
||||
it 'returns a non-local-only post' do
|
||||
subject
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
expect(response.parsed_body[:content]).to match(/Hello world/)
|
||||
expect(response.parsed_body[:local_only]).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'with local_only param omitted' do
|
||||
let(:params) { { status: 'Hello world' } }
|
||||
|
||||
it 'returns a non-local-only post' do
|
||||
subject
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
expect(response.parsed_body[:content]).to match(/Hello world/)
|
||||
expect(response.parsed_body[:local_only]).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'with local_only param omitted in reply to a local-only post' do
|
||||
let(:local_only_post) { Fabricate(:status, local_only: true) }
|
||||
let(:params) { { status: 'Hello world', in_reply_to_id: local_only_post.id } }
|
||||
|
||||
it 'returns a non-local-only post' do
|
||||
subject
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
expect(response.parsed_body[:content]).to match(/Hello world/)
|
||||
expect(response.parsed_body[:local_only]).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/statuses/:id' do
|
||||
|
||||
@@ -71,7 +71,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService do
|
||||
let(:public_key_id) { 'https://example.com/alice-public-key.json' }
|
||||
|
||||
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'] })), headers: { 'Content-Type': 'application/activity+json' })
|
||||
stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' })
|
||||
end
|
||||
|
||||
it 'returns the expected account' do
|
||||
|
||||
@@ -259,6 +259,8 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||
{ type: 'Hashtag', name: 'foo' },
|
||||
{ type: 'Hashtag', name: 'bar' },
|
||||
{ type: 'Hashtag', name: '#2024' },
|
||||
{ type: 'Hashtag', name: 'Foo Bar' },
|
||||
{ type: 'Hashtag', name: 'FooBar' },
|
||||
],
|
||||
}
|
||||
end
|
||||
@@ -270,7 +272,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||
|
||||
it 'updates tags and featured tags' do
|
||||
expect { subject.call(status, json, json) }
|
||||
.to change { status.tags.reload.pluck(:name) }.from(contain_exactly('test', 'foo')).to(contain_exactly('foo', 'bar'))
|
||||
.to change { status.tags.reload.pluck(:name) }.from(contain_exactly('test', 'foo')).to(contain_exactly('foo', 'bar', 'foobar'))
|
||||
.and change { status.account.featured_tags.find_by(name: 'test').statuses_count }.by(-1)
|
||||
.and change { status.account.featured_tags.find_by(name: 'bar').statuses_count }.by(1)
|
||||
.and change { status.account.featured_tags.find_by(name: 'bar').last_status_at }.from(nil).to(be_present)
|
||||
|
||||
13
spec/support/examples/fasp/api.rb
Normal file
13
spec/support/examples/fasp/api.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'forbidden for unconfirmed provider' do
|
||||
context 'when the requesting provider is unconfirmed' do
|
||||
let(:provider) { Fabricate(:fasp_provider) }
|
||||
|
||||
it 'returns http unauthorized' do
|
||||
subject
|
||||
|
||||
expect(response).to have_http_status(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,10 +8,10 @@ RSpec.describe Fasp::AnnounceAccountLifecycleEventWorker do
|
||||
subject { described_class.new.perform(account_uri, 'new') }
|
||||
|
||||
let(:account_uri) { 'https://masto.example.com/accounts/1' }
|
||||
let(:provider) { Fabricate(:confirmed_fasp) }
|
||||
let(:subscription) do
|
||||
Fabricate(:fasp_subscription, category: 'account')
|
||||
Fabricate(:fasp_subscription, fasp_provider: provider, category: 'account')
|
||||
end
|
||||
let(:provider) { subscription.fasp_provider }
|
||||
let(:path) { '/data_sharing/v0/announcements' }
|
||||
|
||||
let!(:stubbed_request) do
|
||||
|
||||
@@ -8,10 +8,10 @@ RSpec.describe Fasp::AnnounceContentLifecycleEventWorker do
|
||||
subject { described_class.new.perform(status_uri, 'new') }
|
||||
|
||||
let(:status_uri) { 'https://masto.example.com/status/1' }
|
||||
let(:provider) { Fabricate(:confirmed_fasp) }
|
||||
let(:subscription) do
|
||||
Fabricate(:fasp_subscription)
|
||||
Fabricate(:fasp_subscription, fasp_provider: provider)
|
||||
end
|
||||
let(:provider) { subscription.fasp_provider }
|
||||
let(:path) { '/data_sharing/v0/announcements' }
|
||||
|
||||
let!(:stubbed_request) do
|
||||
|
||||
@@ -8,14 +8,15 @@ RSpec.describe Fasp::AnnounceTrendWorker do
|
||||
subject { described_class.new.perform(status.id, 'favourite') }
|
||||
|
||||
let(:status) { Fabricate(:status) }
|
||||
let(:provider) { Fabricate(:confirmed_fasp) }
|
||||
let(:subscription) do
|
||||
Fabricate(:fasp_subscription,
|
||||
fasp_provider: provider,
|
||||
category: 'content',
|
||||
subscription_type: 'trends',
|
||||
threshold_timeframe: 15,
|
||||
threshold_likes: 2)
|
||||
end
|
||||
let(:provider) { subscription.fasp_provider }
|
||||
let(:path) { '/data_sharing/v0/announcements' }
|
||||
|
||||
let!(:stubbed_request) do
|
||||
|
||||
@@ -7,8 +7,8 @@ RSpec.describe Fasp::BackfillWorker do
|
||||
|
||||
subject { described_class.new.perform(backfill_request.id) }
|
||||
|
||||
let(:backfill_request) { Fabricate(:fasp_backfill_request) }
|
||||
let(:provider) { backfill_request.fasp_provider }
|
||||
let(:provider) { Fabricate(:confirmed_fasp) }
|
||||
let(:backfill_request) { Fabricate(:fasp_backfill_request, fasp_provider: provider) }
|
||||
let(:status) { Fabricate(:status) }
|
||||
let(:path) { '/data_sharing/v0/announcements' }
|
||||
|
||||
|
||||
33
spec/workers/purge_custom_emoji_worker_spec.rb
Normal file
33
spec/workers/purge_custom_emoji_worker_spec.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PurgeCustomEmojiWorker do
|
||||
let(:worker) { described_class.new }
|
||||
|
||||
let(:domain) { 'evil' }
|
||||
|
||||
before do
|
||||
Fabricate(:custom_emoji)
|
||||
Fabricate(:custom_emoji, domain: 'example.com')
|
||||
Fabricate.times(5, :custom_emoji, domain: domain)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when domain is nil' do
|
||||
it 'does not delete emojis' do
|
||||
expect { worker.perform(nil) }
|
||||
.to_not(change(CustomEmoji, :count))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing a domain' do
|
||||
it 'deletes emojis from this domain only' do
|
||||
expect { worker.perform(domain) }
|
||||
.to change { CustomEmoji.where(domain: domain).count }.to(0)
|
||||
.and not_change { CustomEmoji.local.count }
|
||||
.and(not_change { CustomEmoji.where(domain: 'example.com').count })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -375,6 +375,7 @@ const startServer = async () => {
|
||||
req.scopes = result.rows[0].scopes.split(' ');
|
||||
req.accountId = result.rows[0].account_id;
|
||||
req.chosenLanguages = result.rows[0].chosen_languages;
|
||||
req.permissions = result.rows[0].permissions;
|
||||
|
||||
return {
|
||||
accessTokenId: result.rows[0].id,
|
||||
@@ -600,13 +601,13 @@ const startServer = async () => {
|
||||
|
||||
/**
|
||||
* @param {string} kind
|
||||
* @param {ResolvedAccount} account
|
||||
* @param {Request} req
|
||||
* @returns {Promise.<{ localAccess: boolean, remoteAccess: boolean }>}
|
||||
*/
|
||||
const getFeedAccessSettings = async (kind, account) => {
|
||||
const getFeedAccessSettings = async (kind, req) => {
|
||||
const access = { localAccess: true, remoteAccess: true };
|
||||
|
||||
if (account.permissions & PERMISSION_VIEW_FEEDS) {
|
||||
if (req.permissions & PERMISSION_VIEW_FEEDS) {
|
||||
return access;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user