mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 11:11:11 +02:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04903a45ce | ||
|
|
d9bad1c407 | ||
|
|
cbb1085855 | ||
|
|
3920feb8bd | ||
|
|
4dbe15654a | ||
|
|
27e06cdf20 | ||
|
|
6ac8b52ccc | ||
|
|
7ee99bbe81 | ||
|
|
6c1e77ff1f | ||
|
|
2588d1ab47 | ||
|
|
c0f8640252 | ||
|
|
92be0fd12e | ||
|
|
8450ebc7e8 | ||
|
|
3d27ec34ac | ||
|
|
d3551e1ab6 | ||
|
|
0b9c741dac | ||
|
|
a8c9923df9 | ||
|
|
f32067dc56 | ||
|
|
36981fadf0 | ||
|
|
b60edf59d8 | ||
|
|
3a87a63abf | ||
|
|
ef4d722d6a | ||
|
|
68e30985ca | ||
|
|
1702290786 | ||
|
|
abb3a02ce2 | ||
|
|
25c79f526a | ||
|
|
f5890040e1 | ||
|
|
740f262e38 | ||
|
|
4d2b6795a4 | ||
|
|
6558a1e07a | ||
|
|
b64101ee64 | ||
|
|
0bde273b3d | ||
|
|
22fe977ffe | ||
|
|
8e945bef2a | ||
|
|
d5f12debe0 | ||
|
|
5d0ec718fd | ||
|
|
c7aa312307 | ||
|
|
dc1d4eda7c | ||
|
|
931a29b4f3 | ||
|
|
99b2307350 | ||
|
|
375f2e6ebf | ||
|
|
f0a1da78ba | ||
|
|
b554ecfcb4 | ||
|
|
50244ba682 | ||
|
|
01cf5c103d | ||
|
|
5bda54d15a | ||
|
|
07f5573cd6 | ||
|
|
2b0b537152 | ||
|
|
c49e261ad0 | ||
|
|
915bcb267f | ||
|
|
ff37011057 | ||
|
|
8f5e95a159 | ||
|
|
16ee628d24 | ||
|
|
64a0b060a8 | ||
|
|
fa52f4361a | ||
|
|
7f7d6697c1 | ||
|
|
c2fb12d22d | ||
|
|
2dc4552229 | ||
|
|
95868643a2 | ||
|
|
7c46fdfbf1 | ||
|
|
8965e1bfa9 | ||
|
|
1e27ab0885 | ||
|
|
cef2c50a71 |
4
.github/workflows/build-releases.yml
vendored
4
.github/workflows/build-releases.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
# Only tag with latest when ran against the latest stable branch
|
||||
# This needs to be updated after each minor version release
|
||||
flavor: |
|
||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.4.') }}
|
||||
latest=false
|
||||
tags: |
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
# Only tag with latest when ran against the latest stable branch
|
||||
# This needs to be updated after each minor version release
|
||||
flavor: |
|
||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.4.') }}
|
||||
latest=false
|
||||
tags: |
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
|
||||
@@ -21,11 +21,11 @@ Metrics/BlockNesting:
|
||||
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 25
|
||||
Enabled: false
|
||||
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 27
|
||||
Enabled: false
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: AllowedVars, DefaultToNil.
|
||||
|
||||
73
CHANGELOG.md
73
CHANGELOG.md
@@ -2,6 +2,79 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.4.12] - 2026-01-20
|
||||
|
||||
### Security
|
||||
|
||||
- Fix missing limits on various federated properties [GHSA-gg8q-rcg7-p79g](https://github.com/mastodon/mastodon/security/advisories/GHSA-gg8q-rcg7-p79g)
|
||||
- Fix remote user suspension bypass [GHSA-5h2f-wg8j-xqwp](https://github.com/mastodon/mastodon/security/advisories/GHSA-5h2f-wg8j-xqwp)
|
||||
- Fix missing length limits on some user-provided fields [GHSA-6x3w-9g92-gvf3](https://github.com/mastodon/mastodon/security/advisories/GHSA-6x3w-9g92-gvf3)
|
||||
- Fix missing access check for push notification settings update [GHSA-f3q8-7vw3-69v4](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q8-7vw3-69v4)
|
||||
|
||||
### Changed
|
||||
|
||||
- Skip tombstone creation on deleting from 404 (#37533 by @ClearlyClaire)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix potential duplicate handling of quote accept/reject/delete (#37537 by @ClearlyClaire)
|
||||
- Fix `FeedManager#filter_from_home` error when handling a reblog of a deleted status (#37486 by @ClearlyClaire)
|
||||
- Fix needlessly complicated SQL query in status batch removal (#37469 by @ClearlyClaire)
|
||||
- Fix `Vary` parsing in cache control enforcement (#37426 by @MegaManSec)
|
||||
- Fix thread-unsafe ActivityPub activity dispatch (#37423 by @MegaManSec)
|
||||
- Fix SignatureParser accepting duplicate parameters in HTTP Signature header (#37375 by @shleeable)
|
||||
|
||||
## [4.4.11] - 2026-01-07
|
||||
|
||||
### Security
|
||||
|
||||
- Fix SSRF protection bypass ([GHSA](https://github.com/mastodon/mastodon/security/advisories/GHSA-xfrj-c749-jxxq))
|
||||
- Fix missing ownership check in severed relationships controller ([GHSA](https://github.com/mastodon/mastodon/security/advisories/GHSA-ww85-x9cp-5v24))
|
||||
|
||||
### Changed
|
||||
|
||||
- Change HTTP Signature verification status from 401 to 503 on temporary failure to get remote actor (#37221 by @ClearlyClaire)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix mentions of domain-blocked users being processed (#37257 by @ClearlyClaire)
|
||||
|
||||
## [4.4.10] - 2025-12-08
|
||||
|
||||
### Security
|
||||
|
||||
- Fix inconsistent error handling leaking information on existence of private posts ([GHSA-gwhw-gcjx-72v8](https://github.com/mastodon/mastodon/security/advisories/GHSA-gwhw-gcjx-72v8))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix YouTube embeds by sending referer (#37126 by @ChaosExAnima)
|
||||
- Fix YouTube iframe not being able to start at a defined time (#26584 by @BrunoViveiros)
|
||||
- Fix streamed quoted polls not being hydrated correctly (#37118 by @ClearlyClaire)
|
||||
- Fix error handling when re-fetching already-known statuses (#37077 by @ClearlyClaire)
|
||||
- Fix known expensive S3 batch delete operation failing because of short timeouts (#37004 by @ClearlyClaire)
|
||||
|
||||
## [4.4.9] - 2025-11-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix `tootctl upgrade storage-schema` failing with `ArgumentError` (#36914 by @shugo)
|
||||
- Fix old previously-undiscovered posts being treated as new when receiving an `Update` (#36848 by @ClearlyClaire)
|
||||
- Fix filters not being applied to quotes in detailed view (#36843 by @ClearlyClaire)
|
||||
|
||||
## [4.4.8] - 2025-10-21
|
||||
|
||||
### Security
|
||||
|
||||
- Fix quote control bypass ([GHSA-8h43-rcqj-wpc6](https://github.com/mastodon/mastodon/security/advisories/GHSA-8h43-rcqj-wpc6))
|
||||
|
||||
## [4.4.7] - 2025-10-15
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix forwarder being called with `nil` status when quote post is soft-deleted (#36463 by @ClearlyClaire)
|
||||
- Fix moderation warning e-mails that include posts (#36462 by @ClearlyClaire)
|
||||
- Fix allow_referrer_origin typo (#36460 by @ShadowJonathan)
|
||||
|
||||
## [4.4.6] - 2025-10-13
|
||||
|
||||
### Security
|
||||
|
||||
@@ -48,3 +48,22 @@ Mastodon requires all `POST` requests to be signed, and MAY require `GET` reques
|
||||
### Additional documentation
|
||||
|
||||
- [Mastodon documentation](https://docs.joinmastodon.org/)
|
||||
|
||||
## Size limits
|
||||
|
||||
Mastodon imposes a few hard limits on federated content.
|
||||
These limits are intended to be very generous and way above what the Mastodon user experience is optimized for, so as to accomodate future changes and unusual or unforeseen usage patterns, while still providing some limits for performance reasons.
|
||||
The following table attempts to summary those limits.
|
||||
|
||||
| Limited property | Size limit | Consequence of exceeding the limit |
|
||||
| ------------------------------------------------------------- | ---------- | ---------------------------------- |
|
||||
| Serialized JSON-LD | 1MB | **Activity is rejected/dropped** |
|
||||
| Profile fields (actor `PropertyValue` attachments) name/value | 2047 | Field name/value is truncated |
|
||||
| Number of profile fields (actor `PropertyValue` attachments) | 50 | Fields list is truncated |
|
||||
| Poll options (number of `anyOf`/`oneOf` in a `Question`) | 500 | Items list is truncated |
|
||||
| Account username (actor `preferredUsername`) length | 2048 | **Actor will be rejected** |
|
||||
| Account display name (actor `name`) length | 2048 | Display name will be truncated |
|
||||
| Account note (actor `summary`) length | 20kB | Account note will be truncated |
|
||||
| Account `attributionDomains` | 256 | List will be truncated |
|
||||
| Account aliases (actor `alsoKnownAs`) | 256 | List will be truncated |
|
||||
| Custom emoji shortcode (`Emoji` `name`) | 2048 | Emoji will be rejected |
|
||||
|
||||
@@ -16,6 +16,5 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
||||
| Version | Supported |
|
||||
| ------- | ---------------- |
|
||||
| 4.4.x | Yes |
|
||||
| 4.3.x | Yes |
|
||||
| 4.2.x | Until 2026-01-08 |
|
||||
| < 4.2 | No |
|
||||
| 4.3.x | Until 2026-05-06 |
|
||||
| < 4.3 | No |
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
class ActivityPub::InboxesController < ActivityPub::BaseController
|
||||
include JsonLdHelper
|
||||
|
||||
before_action :skip_large_payload
|
||||
before_action :skip_unknown_actor_activity
|
||||
before_action :require_actor_signature!
|
||||
skip_before_action :authenticate_user!
|
||||
@@ -16,6 +17,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
||||
|
||||
private
|
||||
|
||||
def skip_large_payload
|
||||
head 413 if request.content_length > ActivityPub::Activity::MAX_JSON_SIZE
|
||||
end
|
||||
|
||||
def skip_unknown_actor_activity
|
||||
head 202 if unknown_affected_account?
|
||||
end
|
||||
|
||||
@@ -22,7 +22,7 @@ class ActivityPub::LikesController < ActivityPub::BaseController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class ActivityPub::SharesController < ActivityPub::BaseController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
|
||||
def set_poll
|
||||
@poll = Poll.find(params[:poll_id])
|
||||
authorize @poll.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController
|
||||
def set_poll
|
||||
@poll = Poll.find(params[:id])
|
||||
authorize @poll.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class Api::V1::Statuses::BaseController < Api::BaseController
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,7 +23,7 @@ class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController
|
||||
bookmark&.destroy!
|
||||
|
||||
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false })
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseControlle
|
||||
|
||||
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }, attributes_map: { @status.id => { favourites_count: count } })
|
||||
render json: @status, serializer: REST::StatusSerializer, relationships: relationships
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,7 +36,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
|
||||
|
||||
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false }, attributes_map: { @reblog.id => { reblogs_count: count } })
|
||||
render json: @reblog, serializer: REST::StatusSerializer, relationships: relationships
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
@@ -45,7 +45,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
|
||||
def set_reblog
|
||||
@reblog = Status.find(params[:status_id])
|
||||
authorize @reblog, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
def set_status
|
||||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController
|
||||
def set_status
|
||||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -62,7 +62,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||
end
|
||||
|
||||
def set_push_subscription
|
||||
@push_subscription = ::Web::PushSubscription.find(params[:id])
|
||||
@push_subscription = ::Web::PushSubscription.where(user_id: active_session.user_id).find(params[:id])
|
||||
end
|
||||
|
||||
def subscription_params
|
||||
|
||||
@@ -21,7 +21,7 @@ class AuthorizeInteractionsController < ApplicationController
|
||||
def set_resource
|
||||
@resource = located_resource
|
||||
authorize(@resource, :show?) if @resource.is_a?(Status)
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ module CacheConcern
|
||||
# from being used as cache keys, while allowing to `Vary` on them (to not serve
|
||||
# anonymous cached data to authenticated requests when authentication matters)
|
||||
def enforce_cache_control!
|
||||
vary = response.headers['Vary']&.split&.map { |x| x.strip.downcase }
|
||||
vary = response.headers['Vary'].to_s.split(',').map { |x| x.strip.downcase }.reject(&:empty?)
|
||||
return unless vary.present? && %w(cookie authorization signature).any? { |header| vary.include?(header) && request.headers[header].present? }
|
||||
|
||||
response.cache_control.replace(private: true, no_store: true)
|
||||
|
||||
@@ -70,10 +70,13 @@ module SignatureVerification
|
||||
rescue Mastodon::SignatureVerificationError => e
|
||||
fail_with! e.message
|
||||
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
|
||||
@signature_verification_failure_code ||= 503
|
||||
fail_with! "Failed to fetch remote data: #{e.message}"
|
||||
rescue Mastodon::UnexpectedResponseError
|
||||
@signature_verification_failure_code ||= 503
|
||||
fail_with! 'Failed to fetch remote data (got unexpected reply from server)'
|
||||
rescue Stoplight::Error::RedLight
|
||||
@signature_verification_failure_code ||= 503
|
||||
fail_with! 'Fetching attempt skipped because of recent connection failure'
|
||||
end
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class MediaController < ApplicationController
|
||||
|
||||
def verify_permitted_status!
|
||||
authorize @media_attachment.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class SeveredRelationshipsController < ApplicationController
|
||||
private
|
||||
|
||||
def set_event
|
||||
@event = AccountRelationshipSeveranceEvent.find(params[:id])
|
||||
@event = AccountRelationshipSeveranceEvent.where(account: current_account).find(params[:id])
|
||||
end
|
||||
|
||||
def following_data
|
||||
|
||||
@@ -59,7 +59,7 @@ class StatusesController < ApplicationController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -78,6 +78,8 @@ export function fetchStatus(id, {
|
||||
dispatch(fetchStatusSuccess(skipLoading));
|
||||
}).catch(error => {
|
||||
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
|
||||
if (error.status === 404)
|
||||
dispatch(deleteFromTimelines(id));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,18 +26,23 @@ const getHostname = url => {
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
const addAutoPlay = html => {
|
||||
const handleIframeUrl = (html, url, providerName) => {
|
||||
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
||||
const iframe = document.querySelector('iframe');
|
||||
const startTime = new URL(url).searchParams.get('t')
|
||||
|
||||
if (iframe) {
|
||||
if (iframe.src.indexOf('?') !== -1) {
|
||||
iframe.src += '&';
|
||||
} else {
|
||||
iframe.src += '?';
|
||||
const iframeUrl = new URL(iframe.src)
|
||||
|
||||
iframeUrl.searchParams.set('autoplay', 1)
|
||||
iframeUrl.searchParams.set('auto_play', 1)
|
||||
|
||||
if (providerName === 'YouTube') {
|
||||
iframeUrl.searchParams.set('start', startTime || '');
|
||||
iframe.referrerPolicy = 'strict-origin-when-cross-origin';
|
||||
}
|
||||
|
||||
iframe.src += 'autoplay=1&auto_play=1';
|
||||
iframe.src = iframeUrl.href
|
||||
|
||||
// DOM parser creates html/body elements around original HTML fragment,
|
||||
// so we need to get innerHTML out of the body and not the entire document
|
||||
@@ -103,7 +108,7 @@ export default class Card extends PureComponent {
|
||||
|
||||
renderVideo () {
|
||||
const { card } = this.props;
|
||||
const content = { __html: addAutoPlay(card.get('html')) };
|
||||
const content = { __html: handleIframeUrl(card.get('html'), card.get('url'), card.get('provider_name')) };
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -421,6 +421,7 @@ export const DetailedStatus: React.FC<{
|
||||
<QuotedStatus
|
||||
quote={status.get('quote')}
|
||||
parentQuotePostId={status.get('id')}
|
||||
contextType='thread'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -64,6 +64,10 @@ const statusTranslateUndo = (state, id) => {
|
||||
});
|
||||
};
|
||||
|
||||
const removeStatusStub = (state, id) => {
|
||||
return state.getIn([id, 'id']) ? state.deleteIn([id, 'isLoading']) : state.delete(id);
|
||||
}
|
||||
|
||||
|
||||
/** @type {ImmutableMap<string, import('flavours/glitch/models/status').Status>} */
|
||||
const initialState = ImmutableMap();
|
||||
@@ -75,11 +79,10 @@ export default function statuses(state = initialState, action) {
|
||||
return state.setIn([action.id, 'isLoading'], true);
|
||||
case STATUS_FETCH_FAIL: {
|
||||
if (action.parentQuotePostId && action.error.status === 404) {
|
||||
return state
|
||||
.delete(action.id)
|
||||
return removeStatusStub(state, action.id)
|
||||
.setIn([action.parentQuotePostId, 'quote', 'state'], 'deleted')
|
||||
} else {
|
||||
return state.delete(action.id);
|
||||
return removeStatusStub(state, action.id);
|
||||
}
|
||||
}
|
||||
case STATUS_IMPORT:
|
||||
|
||||
@@ -78,6 +78,8 @@ export function fetchStatus(id, {
|
||||
dispatch(fetchStatusSuccess(skipLoading));
|
||||
}).catch(error => {
|
||||
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
|
||||
if (error.status === 404)
|
||||
dispatch(deleteFromTimelines(id));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,18 +37,23 @@ const getHostname = url => {
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
const addAutoPlay = html => {
|
||||
const handleIframeUrl = (html, url, providerName) => {
|
||||
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
||||
const iframe = document.querySelector('iframe');
|
||||
const startTime = new URL(url).searchParams.get('t')
|
||||
|
||||
if (iframe) {
|
||||
if (iframe.src.indexOf('?') !== -1) {
|
||||
iframe.src += '&';
|
||||
} else {
|
||||
iframe.src += '?';
|
||||
const iframeUrl = new URL(iframe.src)
|
||||
|
||||
iframeUrl.searchParams.set('autoplay', 1)
|
||||
iframeUrl.searchParams.set('auto_play', 1)
|
||||
|
||||
if (providerName === 'YouTube') {
|
||||
iframeUrl.searchParams.set('start', startTime || '');
|
||||
iframe.referrerPolicy = 'strict-origin-when-cross-origin';
|
||||
}
|
||||
|
||||
iframe.src += 'autoplay=1&auto_play=1';
|
||||
iframe.src = iframeUrl.href
|
||||
|
||||
// DOM parser creates html/body elements around original HTML fragment,
|
||||
// so we need to get innerHTML out of the body and not the entire document
|
||||
@@ -114,7 +119,7 @@ export default class Card extends PureComponent {
|
||||
|
||||
renderVideo () {
|
||||
const { card } = this.props;
|
||||
const content = { __html: addAutoPlay(card.get('html')) };
|
||||
const content = { __html: handleIframeUrl(card.get('html'), card.get('url'), card.get('provider_name')) };
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -384,6 +384,7 @@ export const DetailedStatus: React.FC<{
|
||||
<QuotedStatus
|
||||
quote={status.get('quote')}
|
||||
parentQuotePostId={status.get('id')}
|
||||
contextType='thread'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -64,6 +64,10 @@ const statusTranslateUndo = (state, id) => {
|
||||
});
|
||||
};
|
||||
|
||||
const removeStatusStub = (state, id) => {
|
||||
return state.getIn([id, 'id']) ? state.deleteIn([id, 'isLoading']) : state.delete(id);
|
||||
}
|
||||
|
||||
|
||||
/** @type {ImmutableMap<string, import('mastodon/models/status').Status>} */
|
||||
const initialState = ImmutableMap();
|
||||
@@ -75,11 +79,10 @@ export default function statuses(state = initialState, action) {
|
||||
return state.setIn([action.id, 'isLoading'], true);
|
||||
case STATUS_FETCH_FAIL: {
|
||||
if (action.parentQuotePostId && action.error.status === 404) {
|
||||
return state
|
||||
.delete(action.id)
|
||||
return removeStatusStub(state, action.id)
|
||||
.setIn([action.parentQuotePostId, 'quote', 'state'], 'deleted')
|
||||
} else {
|
||||
return state.delete(action.id);
|
||||
return removeStatusStub(state, action.id);
|
||||
}
|
||||
}
|
||||
case STATUS_IMPORT:
|
||||
|
||||
@@ -5,6 +5,7 @@ class ActivityPub::Activity
|
||||
include Redisable
|
||||
include Lockable
|
||||
|
||||
MAX_JSON_SIZE = 1.megabyte
|
||||
SUPPORTED_TYPES = %w(Note Question).freeze
|
||||
CONVERTED_TYPES = %w(Image Audio Video Article Page Event).freeze
|
||||
|
||||
@@ -21,14 +22,13 @@ class ActivityPub::Activity
|
||||
|
||||
class << self
|
||||
def factory(json, account, **)
|
||||
@json = json
|
||||
klass&.new(json, account, **)
|
||||
klass_for(json)&.new(json, account, **)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def klass
|
||||
case @json['type']
|
||||
def klass_for(json)
|
||||
case json['type']
|
||||
when 'Create'
|
||||
ActivityPub::Activity::Create
|
||||
when 'Announce'
|
||||
|
||||
@@ -56,12 +56,14 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
||||
end
|
||||
|
||||
def revoke_quote
|
||||
@quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account)
|
||||
@quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account, state: [:pending, :accepted])
|
||||
return if @quote.nil?
|
||||
|
||||
ActivityPub::Forwarder.new(@account, @json, @quote.status).forward!
|
||||
ActivityPub::Forwarder.new(@account, @json, @quote.status).forward! if @quote.status.present?
|
||||
|
||||
@quote.reject!
|
||||
DistributionWorker.perform_async(@quote.status_id, { 'update' => true })
|
||||
|
||||
DistributionWorker.perform_async(@quote.status_id, { 'update' => true }) if @quote.status.present?
|
||||
end
|
||||
|
||||
def forwarder
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::Activity::Update < ActivityPub::Activity
|
||||
# Updates to unknown objects older than that are ignored
|
||||
OBJECT_AGE_THRESHOLD = 1.day
|
||||
|
||||
def perform
|
||||
@account.schedule_refresh_if_stale!
|
||||
|
||||
@@ -28,6 +31,10 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
|
||||
|
||||
@status = Status.find_by(uri: object_uri, account_id: @account.id)
|
||||
|
||||
# Ignore updates for old unknown objects, since those are updates we are not interested in
|
||||
# Also ignore unknown objects from suspended users for the same reasons
|
||||
return if @status.nil? && (@account.suspended? || object_too_old?)
|
||||
|
||||
# We may be getting `Create` and `Update` out of order
|
||||
@status ||= ActivityPub::Activity::Create.new(@json, @account, **@options).perform
|
||||
|
||||
@@ -35,4 +42,10 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
|
||||
|
||||
ActivityPub::ProcessStatusUpdateService.new.call(@status, @json, @object, request_id: @options[:request_id])
|
||||
end
|
||||
|
||||
def object_too_old?
|
||||
@object['published'].present? && @object['published'].to_datetime < OBJECT_AGE_THRESHOLD.ago
|
||||
rescue Date::Error
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
class ActivityPub::Parser::PollParser
|
||||
include JsonLdHelper
|
||||
|
||||
# Limit the number of items for performance purposes.
|
||||
# We truncate rather than error out to avoid missing the post entirely.
|
||||
MAX_ITEMS = 500
|
||||
|
||||
def initialize(json)
|
||||
@json = json
|
||||
end
|
||||
@@ -48,6 +52,6 @@ class ActivityPub::Parser::PollParser
|
||||
private
|
||||
|
||||
def items
|
||||
@json['anyOf'] || @json['oneOf']
|
||||
(@json['anyOf'] || @json['oneOf'])&.take(MAX_ITEMS)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -112,10 +112,12 @@ class AttachmentBatch
|
||||
keys.each_slice(LIMIT) do |keys_slice|
|
||||
logger.debug { "Deleting #{keys_slice.size} objects" }
|
||||
|
||||
bucket.delete_objects(delete: {
|
||||
objects: keys_slice.map { |key| { key: key } },
|
||||
quiet: true,
|
||||
})
|
||||
with_overridden_timeout(bucket.client, 120) do
|
||||
bucket.delete_objects(delete: {
|
||||
objects: keys_slice.map { |key| { key: key } },
|
||||
quiet: true,
|
||||
})
|
||||
end
|
||||
rescue => e
|
||||
retries += 1
|
||||
|
||||
@@ -134,6 +136,20 @@ class AttachmentBatch
|
||||
@bucket ||= records.first.public_send(@attachment_names.first).s3_bucket
|
||||
end
|
||||
|
||||
# Currently, the aws-sdk-s3 gem does not offer a way to cleanly override the timeout
|
||||
# per-request. So we change the client's config instead. As this client will likely
|
||||
# be re-used for other jobs, restore its original configuration in an `ensure` block.
|
||||
def with_overridden_timeout(s3_client, longer_read_timeout)
|
||||
original_timeout = s3_client.config.http_read_timeout
|
||||
s3_client.config.http_read_timeout = [original_timeout, longer_read_timeout].max
|
||||
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
s3_client.config.http_read_timeout = original_timeout
|
||||
end
|
||||
end
|
||||
|
||||
def nullified_attributes
|
||||
@attachment_names.flat_map { |attachment_name| NULLABLE_ATTRIBUTES.map { |attribute| "#{attachment_name}_#{attribute}" } & klass.column_names }.index_with(nil)
|
||||
end
|
||||
|
||||
@@ -41,12 +41,17 @@ class ConnectionPool::SharedConnectionPool < ConnectionPool
|
||||
# ConnectionPool 2.4+ calls `checkin(force: true)` after fork.
|
||||
# When this happens, we should remove all connections from Thread.current
|
||||
|
||||
::Thread.current.keys.each do |name| # rubocop:disable Style/HashEachMethods
|
||||
next unless name.to_s.start_with?("#{@key}-")
|
||||
connection_keys = ::Thread.current.keys.select { |key| key.to_s.start_with?("#{@key}-") && !key.to_s.start_with?("#{@key_count}-") }
|
||||
count_keys = ::Thread.current.keys.select { |key| key.to_s.start_with?("#{@key_count}-") }
|
||||
|
||||
@available.push(::Thread.current[name])
|
||||
::Thread.current[name] = nil
|
||||
connection_keys.each do |key|
|
||||
@available.push(::Thread.current[key])
|
||||
::Thread.current[key] = nil
|
||||
end
|
||||
count_keys.each do |key|
|
||||
::Thread.current[key] = nil
|
||||
end
|
||||
|
||||
elsif ::Thread.current[key(preferred_tag)]
|
||||
if ::Thread.current[key_count(preferred_tag)] == 1
|
||||
@available.push(::Thread.current[key(preferred_tag)])
|
||||
|
||||
@@ -500,6 +500,7 @@ class FeedManager
|
||||
return :filter if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
|
||||
return :skip_home if timeline_type != :list && crutches[:exclusive_list_users][status.account_id].present?
|
||||
return :filter if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
|
||||
return :filter if status.reblog? && status.reblog.blank?
|
||||
|
||||
check_for_blocks = crutches[:active_mentions][status.id] || []
|
||||
check_for_blocks.push(status.account_id)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PrivateAddressCheck
|
||||
module_function
|
||||
|
||||
CIDR_LIST = [
|
||||
IP4_CIDR_LIST = [
|
||||
IPAddr.new('0.0.0.0/8'), # Current network (only valid as source address)
|
||||
IPAddr.new('100.64.0.0/10'), # Shared Address Space
|
||||
IPAddr.new('172.16.0.0/12'), # Private network
|
||||
@@ -16,6 +14,9 @@ module PrivateAddressCheck
|
||||
IPAddr.new('224.0.0.0/4'), # IP multicast (former Class D network)
|
||||
IPAddr.new('240.0.0.0/4'), # Reserved (former Class E network)
|
||||
IPAddr.new('255.255.255.255'), # Broadcast
|
||||
].freeze
|
||||
|
||||
CIDR_LIST = (IP4_CIDR_LIST + IP4_CIDR_LIST.map(&:ipv4_mapped) + [
|
||||
IPAddr.new('64:ff9b::/96'), # IPv4/IPv6 translation (RFC 6052)
|
||||
IPAddr.new('100::/64'), # Discard prefix (RFC 6666)
|
||||
IPAddr.new('2001::/32'), # Teredo tunneling
|
||||
@@ -25,7 +26,9 @@ module PrivateAddressCheck
|
||||
IPAddr.new('2002::/16'), # 6to4
|
||||
IPAddr.new('fc00::/7'), # Unique local address
|
||||
IPAddr.new('ff00::/8'), # Multicast
|
||||
].freeze
|
||||
]).freeze
|
||||
|
||||
module_function
|
||||
|
||||
def private_address?(address)
|
||||
address.private? || address.loopback? || address.link_local? || CIDR_LIST.any? { |cidr| cidr.include?(address) }
|
||||
|
||||
@@ -25,9 +25,13 @@ class SignatureParser
|
||||
|
||||
# Use `skip` instead of `scan` as we only care about the subgroups
|
||||
while scanner.skip(PARAM_RE)
|
||||
key = scanner[:key]
|
||||
# Detect a duplicate key
|
||||
raise Mastodon::SignatureVerificationError, 'Error parsing signature with duplicate keys' if params.key?(key)
|
||||
|
||||
# This is not actually correct with regards to quoted pairs, but it's consistent
|
||||
# with our previous implementation, and good enough in practice.
|
||||
params[scanner[:key]] = scanner[:value] || scanner[:quoted_value][1...-1]
|
||||
params[key] = scanner[:value] || scanner[:quoted_value][1...-1]
|
||||
|
||||
scanner.skip(/\s*/)
|
||||
return params if scanner.eos?
|
||||
|
||||
@@ -26,12 +26,7 @@ class StatusCacheHydrator
|
||||
|
||||
def hydrate_non_reblog_payload(empty_payload, account_id, nested: false)
|
||||
empty_payload.tap do |payload|
|
||||
fill_status_payload(payload, @status, account_id, nested:)
|
||||
|
||||
if payload[:poll]
|
||||
payload[:poll][:voted] = @status.account_id == account_id
|
||||
payload[:poll][:own_votes] = []
|
||||
end
|
||||
fill_status_payload(payload, @status, account_id, fresh: !nested, nested:)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,18 +40,7 @@ class StatusCacheHydrator
|
||||
# used to create the status, we need to hydrate it here too
|
||||
payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id
|
||||
|
||||
fill_status_payload(payload[:reblog], @status.reblog, account_id, nested:)
|
||||
|
||||
if payload[:reblog][:poll]
|
||||
if @status.reblog.account_id == account_id
|
||||
payload[:reblog][:poll][:voted] = true
|
||||
payload[:reblog][:poll][:own_votes] = []
|
||||
else
|
||||
own_votes = PollVote.where(poll_id: @status.reblog.poll_id, account_id: account_id).pluck(:choice)
|
||||
payload[:reblog][:poll][:voted] = !own_votes.empty?
|
||||
payload[:reblog][:poll][:own_votes] = own_votes
|
||||
end
|
||||
end
|
||||
fill_status_payload(payload[:reblog], @status.reblog, account_id, fresh: false, nested:)
|
||||
|
||||
payload[:filtered] = payload[:reblog][:filtered]
|
||||
payload[:favourited] = payload[:reblog][:favourited]
|
||||
@@ -64,7 +48,7 @@ class StatusCacheHydrator
|
||||
end
|
||||
end
|
||||
|
||||
def fill_status_payload(payload, status, account_id, nested: false)
|
||||
def fill_status_payload(payload, status, account_id, nested: false, fresh: true)
|
||||
payload[:favourited] = Favourite.exists?(account_id: account_id, status_id: status.id)
|
||||
payload[:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: status.id)
|
||||
payload[:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: status.conversation_id)
|
||||
@@ -72,6 +56,30 @@ class StatusCacheHydrator
|
||||
payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id
|
||||
payload[:filtered] = mapped_applied_custom_filter(account_id, status)
|
||||
payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account_id, nested:) if payload[:quote]
|
||||
|
||||
if payload[:poll]
|
||||
if fresh
|
||||
# If the status is brand new, we don't need to look up votes in database
|
||||
payload[:poll][:voted] = status.account_id == account_id
|
||||
payload[:poll][:own_votes] = []
|
||||
elsif status.account_id == account_id
|
||||
payload[:poll][:voted] = true
|
||||
payload[:poll][:own_votes] = []
|
||||
else
|
||||
own_votes = PollVote.where(poll_id: status.poll_id, account_id: account_id).pluck(:choice)
|
||||
payload[:poll][:voted] = !own_votes.empty?
|
||||
payload[:poll][:own_votes] = own_votes
|
||||
end
|
||||
end
|
||||
|
||||
# Nested statuses are more likely to have a stale cache
|
||||
fill_status_stats(payload, status) if nested
|
||||
end
|
||||
|
||||
def fill_status_stats(payload, status)
|
||||
payload[:replies_count] = status.replies_count
|
||||
payload[:reblogs_count] = status.untrusted_reblogs_count || status.reblogs_count
|
||||
payload[:favourites_count] = status.untrusted_favourites_count || status.favourites_count
|
||||
end
|
||||
|
||||
def hydrate_quote_payload(empty_payload, quote, account_id, nested: false)
|
||||
|
||||
@@ -78,6 +78,13 @@ class Account < ApplicationRecord
|
||||
DISPLAY_NAME_LENGTH_LIMIT = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i
|
||||
NOTE_LENGTH_LIMIT = (ENV['MAX_BIO_CHARS'] || 500).to_i
|
||||
|
||||
# Hard limits for federated content
|
||||
USERNAME_LENGTH_HARD_LIMIT = 2048
|
||||
DISPLAY_NAME_LENGTH_HARD_LIMIT = 2048
|
||||
NOTE_LENGTH_HARD_LIMIT = 20.kilobytes
|
||||
ATTRIBUTION_DOMAINS_HARD_LIMIT = 256
|
||||
ALSO_KNOWN_AS_HARD_LIMIT = 256
|
||||
|
||||
AUTOMATED_ACTOR_TYPES = %w(Application Service).freeze
|
||||
|
||||
include Attachmentable # Load prior to Avatar & Header concerns
|
||||
@@ -109,7 +116,7 @@ class Account < ApplicationRecord
|
||||
validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? }
|
||||
|
||||
# Remote user validations, also applies to internal actors
|
||||
validates :username, format: { with: USERNAME_ONLY_RE }, if: -> { (remote? || actor_type_application?) && will_save_change_to_username? }
|
||||
validates :username, format: { with: USERNAME_ONLY_RE }, length: { maximum: USERNAME_LENGTH_HARD_LIMIT }, if: -> { (remote? || actor_type_application?) && will_save_change_to_username? }
|
||||
|
||||
# Remote user validations
|
||||
validates :uri, presence: true, unless: :local?, on: :create
|
||||
|
||||
@@ -27,6 +27,8 @@ class CustomEmoji < ApplicationRecord
|
||||
LOCAL_LIMIT = (ENV['MAX_EMOJI_SIZE'] || 256.kilobytes).to_i
|
||||
LIMIT = [LOCAL_LIMIT, (ENV['MAX_REMOTE_EMOJI_SIZE'] || 256.kilobytes).to_i].max
|
||||
MINIMUM_SHORTCODE_SIZE = 2
|
||||
MAX_SHORTCODE_SIZE = 128
|
||||
MAX_FEDERATED_SHORTCODE_SIZE = 2048
|
||||
|
||||
SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}'
|
||||
|
||||
@@ -48,7 +50,8 @@ class CustomEmoji < ApplicationRecord
|
||||
validates_attachment :image, content_type: { content_type: IMAGE_MIME_TYPES }, presence: true
|
||||
validates_attachment_size :image, less_than: LIMIT, unless: :local?
|
||||
validates_attachment_size :image, less_than: LOCAL_LIMIT, if: :local?
|
||||
validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: MINIMUM_SHORTCODE_SIZE }
|
||||
validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: MINIMUM_SHORTCODE_SIZE, maximum: MAX_FEDERATED_SHORTCODE_SIZE }
|
||||
validates :shortcode, length: { maximum: MAX_SHORTCODE_SIZE }, if: :local?
|
||||
|
||||
scope :local, -> { where(domain: nil) }
|
||||
scope :remote, -> { where.not(domain: nil) }
|
||||
|
||||
@@ -30,6 +30,8 @@ class CustomFilter < ApplicationRecord
|
||||
|
||||
EXPIRATION_DURATIONS = [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].freeze
|
||||
|
||||
TITLE_LENGTH_LIMIT = 256
|
||||
|
||||
include Expireable
|
||||
include Redisable
|
||||
|
||||
@@ -41,6 +43,7 @@ class CustomFilter < ApplicationRecord
|
||||
accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true
|
||||
|
||||
validates :title, :context, presence: true
|
||||
validates :title, length: { maximum: TITLE_LENGTH_LIMIT }
|
||||
validate :context_must_be_valid
|
||||
|
||||
normalizes :context, with: ->(context) { context.map(&:strip).filter_map(&:presence) }
|
||||
|
||||
@@ -17,7 +17,9 @@ class CustomFilterKeyword < ApplicationRecord
|
||||
|
||||
belongs_to :custom_filter
|
||||
|
||||
validates :keyword, presence: true
|
||||
KEYWORD_LENGTH_LIMIT = 512
|
||||
|
||||
validates :keyword, presence: true, length: { maximum: KEYWORD_LENGTH_LIMIT }
|
||||
|
||||
alias_attribute :phrase, :keyword
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class List < ApplicationRecord
|
||||
include Paginable
|
||||
|
||||
PER_ACCOUNT_LIMIT = 50
|
||||
TITLE_LENGTH_LIMIT = 256
|
||||
|
||||
enum :replies_policy, { list: 0, followed: 1, none: 2 }, prefix: :show, validate: true
|
||||
|
||||
@@ -26,7 +27,7 @@ class List < ApplicationRecord
|
||||
has_many :accounts, through: :list_accounts
|
||||
has_many :active_accounts, -> { merge(ListAccount.active) }, through: :list_accounts, source: :account
|
||||
|
||||
validates :title, presence: true
|
||||
validates :title, presence: true, length: { maximum: TITLE_LENGTH_LIMIT }
|
||||
|
||||
validate :validate_account_lists_limit, on: :create
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class Quote < ApplicationRecord
|
||||
before_validation :set_activity_uri, only: :create, if: -> { account.local? && quoted_account&.remote? }
|
||||
validates :activity_uri, presence: true, if: -> { account.local? && quoted_account&.remote? }
|
||||
validate :validate_visibility
|
||||
validate :validate_original_quoted_status
|
||||
|
||||
def accept!
|
||||
update!(state: :accepted)
|
||||
@@ -41,9 +42,9 @@ class Quote < ApplicationRecord
|
||||
|
||||
def reject!
|
||||
if accepted?
|
||||
update!(state: :revoked)
|
||||
update!(state: :revoked, approval_uri: nil)
|
||||
elsif !revoked?
|
||||
update!(state: :rejected)
|
||||
update!(state: :rejected, approval_uri: nil)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -70,6 +71,10 @@ class Quote < ApplicationRecord
|
||||
errors.add(:quoted_status_id, :visibility_mismatch)
|
||||
end
|
||||
|
||||
def validate_original_quoted_status
|
||||
errors.add(:quoted_status_id, :reblog_unallowed) if quoted_status&.reblog?
|
||||
end
|
||||
|
||||
def set_activity_uri
|
||||
self.activity_uri = [ActivityPub::TagManager.instance.uri_for(account), '/quote_requests/', SecureRandom.uuid].join
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ class REST::BaseQuoteSerializer < ActiveModel::Serializer
|
||||
end
|
||||
|
||||
def quoted_status
|
||||
object.quoted_status if object.accepted? && object.quoted_status.present? && !status_filter.filtered_for_quote?
|
||||
object.quoted_status if object.accepted? && object.quoted_status.present? && !object.quoted_status&.reblog? && !status_filter.filtered_for_quote?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -92,7 +92,6 @@ class ActivityPub::FetchRemoteStatusService < BaseService
|
||||
existing_status = Status.remote.find_by(uri: uri)
|
||||
if existing_status&.distributable?
|
||||
Rails.logger.debug { "FetchRemoteStatusService - Got 404 for orphaned status with URI #{uri}, deleting" }
|
||||
Tombstone.find_or_create_by(uri: uri, account: existing_status.account)
|
||||
RemoveStatusService.new.call(existing_status, redraft: false)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||
include Redisable
|
||||
include Lockable
|
||||
|
||||
MAX_PROFILE_FIELDS = 50
|
||||
SUBDOMAINS_RATELIMIT = 10
|
||||
DISCOVERIES_PER_REQUEST = 400
|
||||
|
||||
@@ -122,15 +123,15 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||
|
||||
def set_immediate_attributes!
|
||||
@account.featured_collection_url = valid_collection_uri(@json['featured'])
|
||||
@account.display_name = @json['name'] || ''
|
||||
@account.note = @json['summary'] || ''
|
||||
@account.display_name = (@json['name'] || '')[0...(Account::DISPLAY_NAME_LENGTH_HARD_LIMIT)]
|
||||
@account.note = (@json['summary'] || '')[0...(Account::NOTE_LENGTH_HARD_LIMIT)]
|
||||
@account.locked = @json['manuallyApprovesFollowers'] || false
|
||||
@account.fields = property_values || {}
|
||||
@account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) }
|
||||
@account.also_known_as = as_array(@json['alsoKnownAs'] || []).take(Account::ALSO_KNOWN_AS_HARD_LIMIT).map { |item| value_or_id(item) }
|
||||
@account.discoverable = @json['discoverable'] || false
|
||||
@account.indexable = @json['indexable'] || false
|
||||
@account.memorial = @json['memorial'] || false
|
||||
@account.attribution_domains = as_array(@json['attributionDomains'] || []).map { |item| value_or_id(item) }
|
||||
@account.attribution_domains = as_array(@json['attributionDomains'] || []).take(Account::ATTRIBUTION_DOMAINS_HARD_LIMIT).map { |item| value_or_id(item) }
|
||||
end
|
||||
|
||||
def set_fetchable_key!
|
||||
@@ -251,7 +252,10 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||
def property_values
|
||||
return unless @json['attachment'].is_a?(Array)
|
||||
|
||||
as_array(@json['attachment']).select { |attachment| attachment['type'] == 'PropertyValue' }.map { |attachment| attachment.slice('name', 'value') }
|
||||
as_array(@json['attachment'])
|
||||
.select { |attachment| attachment['type'] == 'PropertyValue' }
|
||||
.take(MAX_PROFILE_FIELDS)
|
||||
.map { |attachment| attachment.slice('name', 'value') }
|
||||
end
|
||||
|
||||
def mismatching_origin?(url)
|
||||
|
||||
@@ -72,7 +72,7 @@ class ActivityPub::VerifyQuoteService < BaseService
|
||||
|
||||
status ||= ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: @quote.account.followers.local.first, prefetched_body:, request_id: @request_id, depth: @depth + 1)
|
||||
|
||||
@quote.update(quoted_status: status) if status.present?
|
||||
@quote.update(quoted_status: status) if status.present? && !status.reblog?
|
||||
rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e
|
||||
@fetching_error = e
|
||||
end
|
||||
@@ -90,7 +90,7 @@ class ActivityPub::VerifyQuoteService < BaseService
|
||||
|
||||
status = ActivityPub::FetchRemoteStatusService.new.call(object['id'], prefetched_body: object, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id, depth: @depth)
|
||||
|
||||
if status.present?
|
||||
if status.present? && !status.reblog?
|
||||
@quote.update(quoted_status: status)
|
||||
true
|
||||
else
|
||||
|
||||
@@ -34,7 +34,7 @@ class BatchedRemoveStatusService < BaseService
|
||||
# transaction lock the database, but we use the delete method instead
|
||||
# of destroy to avoid all callbacks. We rely on foreign keys to
|
||||
# cascade the delete faster without loading the associations.
|
||||
statuses_and_reblogs.each_slice(50) { |slice| Status.where(id: slice.map(&:id)).delete_all }
|
||||
statuses_and_reblogs.each_slice(50) { |slice| Status.unscoped.where(id: slice.pluck(:id)).delete_all }
|
||||
|
||||
# Since we skipped all callbacks, we also need to manually
|
||||
# deindex the statuses
|
||||
|
||||
@@ -14,6 +14,8 @@ class FanOutOnWriteService < BaseService
|
||||
@account = status.account
|
||||
@options = options
|
||||
|
||||
return if @status.proper.account.suspended?
|
||||
|
||||
check_race_condition!
|
||||
warm_payload_cache!
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ class ProcessMentionsService < BaseService
|
||||
# Make sure we never mention blocked accounts
|
||||
unless @current_mentions.empty?
|
||||
mentioned_domains = @current_mentions.filter_map { |m| m.account.domain }.uniq
|
||||
blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains))
|
||||
blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains).pluck(:domain))
|
||||
mentioned_account_ids = @current_mentions.map(&:account_id)
|
||||
blocked_account_ids = Set.new(@status.account.block_relationships.where(target_account_id: mentioned_account_ids).pluck(:target_account_id))
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||
%tr
|
||||
%td.email-status-content
|
||||
= render 'status_content', status: status
|
||||
= render 'notification_mailer/status_content', status: status
|
||||
|
||||
%p.email-status-footer
|
||||
= link_to l(status.created_at.in_time_zone(time_zone.presence), format: :with_time_zone), web_url("@#{status.account.pretty_acct}/#{status.id}")
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||
%tr
|
||||
%td.email-status-content
|
||||
= render 'status_content', status: status
|
||||
= render 'notification_mailer/status_content', status: status
|
||||
|
||||
- if status.local? && status.quote
|
||||
%table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||
%tr
|
||||
%td.email-inner-nested-card-td
|
||||
= render 'nested_quote', status: status.quote.quoted_status, time_zone: time_zone
|
||||
= render 'notification_mailer/nested_quote', status: status.quote.quoted_status, time_zone: time_zone
|
||||
%p.email-status-footer
|
||||
= link_to l(status.created_at.in_time_zone(time_zone.presence), format: :with_time_zone), web_url("@#{status.account.pretty_acct}/#{status.id}")
|
||||
|
||||
@@ -58,7 +58,7 @@ defaults: &defaults
|
||||
require_invite_text: false
|
||||
backups_retention_period: 7
|
||||
captcha_enabled: false
|
||||
allow_referer_origin: false
|
||||
allow_referrer_origin: false
|
||||
|
||||
development:
|
||||
<<: *defaults
|
||||
|
||||
@@ -27,7 +27,7 @@ services:
|
||||
|
||||
# es:
|
||||
# restart: always
|
||||
# image: docker.elastic.co/elasticsearch/elasticsearch:7.17.4
|
||||
# image: docker.elastic.co/elasticsearch/elasticsearch:7.17.29
|
||||
# environment:
|
||||
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m -Des.enforce.bootstrap.checks=true"
|
||||
# - "xpack.license.self_generated.type=basic"
|
||||
@@ -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.4.6
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.4.12
|
||||
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.4.6
|
||||
image: ghcr.io/glitch-soc/mastodon-streaming:v4.4.12
|
||||
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.4.6
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.4.12
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec sidekiq
|
||||
|
||||
@@ -123,12 +123,12 @@ module Mastodon::CLI
|
||||
progress.log("Moving #{previous_path} to #{upgraded_path}") if options[:verbose]
|
||||
|
||||
begin
|
||||
move_previous_to_upgraded
|
||||
move_previous_to_upgraded(previous_path, upgraded_path)
|
||||
rescue => e
|
||||
progress.log(pastel.red("Error processing #{previous_path}: #{e}"))
|
||||
success = false
|
||||
|
||||
remove_directory
|
||||
remove_directory(upgraded_path)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ module Mastodon
|
||||
end
|
||||
|
||||
def patch
|
||||
6
|
||||
12
|
||||
end
|
||||
|
||||
def default_prerelease
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"http-link-header": "^1.1.1",
|
||||
"immutable": "^4.3.0",
|
||||
"intl-messageformat": "^10.7.16",
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"lande": "^1.0.10",
|
||||
"lodash": "^4.17.21",
|
||||
"marky": "^1.2.5",
|
||||
|
||||
@@ -1096,6 +1096,60 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a quote of a known reblog that is otherwise valid' do
|
||||
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||
let(:quoted_status) { Fabricate(:status, account: quoted_account, reblog: Fabricate(:status)) }
|
||||
let(:approval_uri) { 'https://quoted.example.com/quote-approval' }
|
||||
|
||||
let(:object_json) do
|
||||
build_object(
|
||||
type: 'Note',
|
||||
content: 'woah what she said is amazing',
|
||||
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||
quoteAuthorization: approval_uri
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
{
|
||||
QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization',
|
||||
gts: 'https://gotosocial.org/ns#',
|
||||
interactionPolicy: {
|
||||
'@id': 'gts:interactionPolicy',
|
||||
'@type': '@id',
|
||||
},
|
||||
interactingObject: {
|
||||
'@id': 'gts:interactingObject',
|
||||
'@type': '@id',
|
||||
},
|
||||
interactionTarget: {
|
||||
'@id': 'gts:interactionTarget',
|
||||
'@type': '@id',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'QuoteAuthorization',
|
||||
id: approval_uri,
|
||||
attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account),
|
||||
interactingObject: object_json[:id],
|
||||
interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||
}))
|
||||
end
|
||||
|
||||
it 'creates a status without the verified quote' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
expect(status.quote).to_not be_nil
|
||||
expect(status.quote.state).to_not eq 'accepted'
|
||||
expect(status.quote.quoted_status).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a vote to a local poll' do
|
||||
let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) }
|
||||
let!(:local_status) { Fabricate(:status, poll: poll) }
|
||||
|
||||
@@ -34,6 +34,8 @@ RSpec.describe ActivityPub::Activity do
|
||||
}
|
||||
end
|
||||
|
||||
let(:publication_date) { 1.hour.ago.utc }
|
||||
|
||||
let(:create_json) do
|
||||
{
|
||||
'@context': [
|
||||
@@ -52,7 +54,7 @@ RSpec.describe ActivityPub::Activity do
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
],
|
||||
content: 'foo',
|
||||
published: '2025-05-24T11:03:10Z',
|
||||
published: publication_date.iso8601,
|
||||
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||
},
|
||||
}.deep_stringify_keys
|
||||
@@ -77,7 +79,7 @@ RSpec.describe ActivityPub::Activity do
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
],
|
||||
content: 'foo',
|
||||
published: '2025-05-24T11:03:10Z',
|
||||
published: publication_date.iso8601,
|
||||
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||
quoteAuthorization: approval_uri,
|
||||
},
|
||||
|
||||
20
spec/lib/private_address_check_spec.rb
Normal file
20
spec/lib/private_address_check_spec.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PrivateAddressCheck do
|
||||
describe 'private_address?' do
|
||||
it 'returns true for private addresses' do
|
||||
# rubocop:disable RSpec/ExpectActual
|
||||
expect(
|
||||
[
|
||||
'192.168.1.7',
|
||||
'0.0.0.0',
|
||||
'127.0.0.1',
|
||||
'::ffff:0.0.0.1',
|
||||
]
|
||||
).to all satisfy('return true') { |addr| described_class.private_address?(IPAddr.new(addr)) }
|
||||
# rubocop:enable RSpec/ExpectActual
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -164,6 +164,28 @@ RSpec.describe StatusCacheHydrator do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the quoted post has a poll authored by the user' do
|
||||
let(:poll) { Fabricate(:poll, account: account) }
|
||||
let(:quoted_status) { Fabricate(:status, poll: poll, account: account) }
|
||||
|
||||
it 'renders the same attributes as a full render' do
|
||||
expect(subject).to eql(compare_to_hash)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the quoted post has been voted in' do
|
||||
let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) }
|
||||
let(:quoted_status) { Fabricate(:status, poll: poll) }
|
||||
|
||||
before do
|
||||
VoteService.new.call(account, poll, [0])
|
||||
end
|
||||
|
||||
it 'renders the same attributes as a full render' do
|
||||
expect(subject).to eql(compare_to_hash)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the quoted post matches account filters' do
|
||||
let(:quoted_status) { Fabricate(:status, text: 'this toot is about that banned word') }
|
||||
|
||||
|
||||
@@ -141,7 +141,9 @@ RSpec.describe UserMailer do
|
||||
end
|
||||
|
||||
describe '#warning' do
|
||||
let(:strike) { Fabricate(:account_warning, target_account: receiver.account, text: 'dont worry its just the testsuite', action: 'suspend') }
|
||||
let(:status) { Fabricate(:status, account: receiver.account) }
|
||||
let(:quote) { Fabricate(:quote, state: :accepted, status: status) }
|
||||
let(:strike) { Fabricate(:account_warning, target_account: receiver.account, text: 'dont worry its just the testsuite', action: 'suspend', status_ids: [quote.status_id]) }
|
||||
let(:mail) { described_class.warning(receiver, strike) }
|
||||
|
||||
it 'renders warning notification' do
|
||||
|
||||
@@ -65,9 +65,10 @@ RSpec.describe 'API Web Push Subscriptions' do
|
||||
end
|
||||
|
||||
describe 'PUT /api/web/push_subscriptions/:id' do
|
||||
before { sign_in Fabricate :user }
|
||||
before { sign_in user }
|
||||
|
||||
let(:subscription) { Fabricate :web_push_subscription }
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:subscription) { Fabricate(:web_push_subscription, user: user) }
|
||||
|
||||
it 'gracefully handles invalid nested params' do
|
||||
put api_web_push_subscription_path(subscription), params: { data: 'invalid' }
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Severed Relationships' do
|
||||
let(:account_rs_event) { Fabricate :account_relationship_severance_event }
|
||||
let(:account_rs_event) { Fabricate(:account_relationship_severance_event) }
|
||||
let(:user) { account_rs_event.account.user }
|
||||
|
||||
before { sign_in Fabricate(:user) }
|
||||
before { sign_in user }
|
||||
|
||||
describe 'GET /severed_relationships/:id/following' do
|
||||
it 'returns a CSV file with correct data' do
|
||||
@@ -22,6 +23,17 @@ RSpec.describe 'Severed Relationships' do
|
||||
expect(response.body)
|
||||
.to include('Account address')
|
||||
end
|
||||
|
||||
context 'when the user is not the subject of the event' do
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
it 'returns a 404' do
|
||||
get following_severed_relationship_path(account_rs_event, format: :csv)
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /severed_relationships/:id/followers' do
|
||||
@@ -39,5 +51,16 @@ RSpec.describe 'Severed Relationships' do
|
||||
expect(response.body)
|
||||
.to include('Account address')
|
||||
end
|
||||
|
||||
context 'when the user is not the subject of the event' do
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
it 'returns a 404' do
|
||||
get followers_severed_relationship_path(account_rs_event, format: :csv)
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -733,6 +733,72 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status adds a verifiable quote of a reblog through an explicit update' do
|
||||
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||
let(:quoted_status) { Fabricate(:status, account: quoted_account, reblog: Fabricate(:status)) }
|
||||
let(:approval_uri) { 'https://quoted.example.com/approvals/1' }
|
||||
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
{
|
||||
'@id': 'https://w3id.org/fep/044f#quote',
|
||||
'@type': '@id',
|
||||
},
|
||||
{
|
||||
'@id': 'https://w3id.org/fep/044f#quoteAuthorization',
|
||||
'@type': '@id',
|
||||
},
|
||||
],
|
||||
id: 'foo',
|
||||
type: 'Note',
|
||||
summary: 'Show more',
|
||||
content: 'Hello universe',
|
||||
updated: '2021-09-08T22:39:25Z',
|
||||
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||
quoteAuthorization: approval_uri,
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
{
|
||||
QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization',
|
||||
gts: 'https://gotosocial.org/ns#',
|
||||
interactionPolicy: {
|
||||
'@id': 'gts:interactionPolicy',
|
||||
'@type': '@id',
|
||||
},
|
||||
interactingObject: {
|
||||
'@id': 'gts:interactingObject',
|
||||
'@type': '@id',
|
||||
},
|
||||
interactionTarget: {
|
||||
'@id': 'gts:interactionTarget',
|
||||
'@type': '@id',
|
||||
},
|
||||
},
|
||||
],
|
||||
type: 'QuoteAuthorization',
|
||||
id: approval_uri,
|
||||
attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account),
|
||||
interactingObject: ActivityPub::TagManager.instance.uri_for(status),
|
||||
interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||
}))
|
||||
end
|
||||
|
||||
it 'updates the approval URI but does not verify the quote' do
|
||||
expect { subject.call(status, json, json) }
|
||||
.to change(status, :quote).from(nil)
|
||||
expect(status.quote.approval_uri).to eq approval_uri
|
||||
expect(status.quote.state).to_not eq 'accepted'
|
||||
expect(status.quote.quoted_status).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status adds a unverifiable quote through an implicit update' do
|
||||
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||
|
||||
@@ -8,9 +8,9 @@ RSpec.describe ProcessMentionsService do
|
||||
let(:account) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
context 'when mentions contain blocked accounts' do
|
||||
let(:non_blocked_account) { Fabricate(:account) }
|
||||
let(:individually_blocked_account) { Fabricate(:account) }
|
||||
let(:domain_blocked_account) { Fabricate(:account, domain: 'evil.com') }
|
||||
let!(:non_blocked_account) { Fabricate(:account) }
|
||||
let!(:individually_blocked_account) { Fabricate(:account) }
|
||||
let!(:domain_blocked_account) { Fabricate(:account, domain: 'evil.com', protocol: :activitypub) }
|
||||
let(:status) { Fabricate(:status, account: account, text: "Hello @#{non_blocked_account.acct} @#{individually_blocked_account.acct} @#{domain_blocked_account.acct}", visibility: :public) }
|
||||
|
||||
before do
|
||||
|
||||
19
yarn.lock
19
yarn.lock
@@ -2689,7 +2689,7 @@ __metadata:
|
||||
husky: "npm:^9.0.11"
|
||||
immutable: "npm:^4.3.0"
|
||||
intl-messageformat: "npm:^10.7.16"
|
||||
js-yaml: "npm:^4.1.0"
|
||||
js-yaml: "npm:^4.1.1"
|
||||
lande: "npm:^1.0.10"
|
||||
lint-staged: "npm:^16.0.0"
|
||||
lodash: "npm:^4.17.21"
|
||||
@@ -7769,8 +7769,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.4.1":
|
||||
version: 10.4.5
|
||||
resolution: "glob@npm:10.4.5"
|
||||
version: 10.5.0
|
||||
resolution: "glob@npm:10.5.0"
|
||||
dependencies:
|
||||
foreground-child: "npm:^3.1.0"
|
||||
jackspeak: "npm:^3.1.2"
|
||||
@@ -7780,7 +7780,7 @@ __metadata:
|
||||
path-scurry: "npm:^1.11.1"
|
||||
bin:
|
||||
glob: dist/esm/bin.mjs
|
||||
checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e
|
||||
checksum: 10c0/100705eddbde6323e7b35e1d1ac28bcb58322095bd8e63a7d0bef1a2cdafe0d0f7922a981b2b48369a4f8c1b077be5c171804534c3509dfe950dde15fbe6d828
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8762,6 +8762,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:^4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "js-yaml@npm:4.1.1"
|
||||
dependencies:
|
||||
argparse: "npm:^2.0.1"
|
||||
bin:
|
||||
js-yaml: bin/js-yaml.js
|
||||
checksum: 10c0/561c7d7088c40a9bb53cc75becbfb1df6ae49b34b5e6e5a81744b14ae8667ec564ad2527709d1a6e7d5e5fa6d483aa0f373a50ad98d42fde368ec4a190d4fae7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsdoc-type-pratt-parser@npm:~4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "jsdoc-type-pratt-parser@npm:4.1.0"
|
||||
|
||||
Reference in New Issue
Block a user