mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-14 08:19:05 +00:00
Compare commits
25 Commits
v4.4.8
...
stable-4.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -2,6 +2,28 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [4.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
|
## [4.4.8] - 2025-10-21
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
|||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ---------------- |
|
| ------- | ---------------- |
|
||||||
| 4.4.x | Yes |
|
| 4.4.x | Yes |
|
||||||
| 4.3.x | Yes |
|
| 4.3.x | Until 2026-05-06 |
|
||||||
| 4.2.x | Until 2026-01-08 |
|
| 4.2.x | Until 2026-01-08 |
|
||||||
| < 4.2 | No |
|
| < 4.2 | No |
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ActivityPub::LikesController < ActivityPub::BaseController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = @account.statuses.find(params[:status_id])
|
@status = @account.statuses.find(params[:status_id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = @account.statuses.find(params[:status_id])
|
@status = @account.statuses.find(params[:status_id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ActivityPub::SharesController < ActivityPub::BaseController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = @account.statuses.find(params[:status_id])
|
@status = @account.statuses.find(params[:status_id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
|
|||||||
def set_poll
|
def set_poll
|
||||||
@poll = Poll.find(params[:poll_id])
|
@poll = Poll.find(params[:poll_id])
|
||||||
authorize @poll.status, :show?
|
authorize @poll.status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController
|
|||||||
def set_poll
|
def set_poll
|
||||||
@poll = Poll.find(params[:id])
|
@poll = Poll.find(params[:id])
|
||||||
authorize @poll.status, :show?
|
authorize @poll.status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class Api::V1::Statuses::BaseController < Api::BaseController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = Status.find(params[:status_id])
|
@status = Status.find(params[:status_id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController
|
|||||||
bookmark&.destroy!
|
bookmark&.destroy!
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false })
|
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
|
not_found
|
||||||
end
|
end
|
||||||
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 } })
|
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
|
render json: @status, serializer: REST::StatusSerializer, relationships: relationships
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
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 } })
|
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
|
render json: @reblog, serializer: REST::StatusSerializer, relationships: relationships
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
|
|||||||
def set_reblog
|
def set_reblog
|
||||||
@reblog = Status.find(params[:status_id])
|
@reblog = Status.find(params[:status_id])
|
||||||
authorize @reblog, :show?
|
authorize @reblog, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = Status.find(params[:id])
|
@status = Status.find(params[:id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = Status.find(params[:id])
|
@status = Status.find(params[:id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class AuthorizeInteractionsController < ApplicationController
|
|||||||
def set_resource
|
def set_resource
|
||||||
@resource = located_resource
|
@resource = located_resource
|
||||||
authorize(@resource, :show?) if @resource.is_a?(Status)
|
authorize(@resource, :show?) if @resource.is_a?(Status)
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class MediaController < ApplicationController
|
|||||||
|
|
||||||
def verify_permitted_status!
|
def verify_permitted_status!
|
||||||
authorize @media_attachment.status, :show?
|
authorize @media_attachment.status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class StatusesController < ApplicationController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = @account.statuses.find(params[:id])
|
@status = @account.statuses.find(params[:id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ export function fetchStatus(id, {
|
|||||||
dispatch(fetchStatusSuccess(skipLoading));
|
dispatch(fetchStatusSuccess(skipLoading));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
|
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 domParser = new DOMParser();
|
||||||
|
|
||||||
const addAutoPlay = html => {
|
const handleIframeUrl = (html, url, providerName) => {
|
||||||
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
||||||
const iframe = document.querySelector('iframe');
|
const iframe = document.querySelector('iframe');
|
||||||
|
const startTime = new URL(url).searchParams.get('t')
|
||||||
|
|
||||||
if (iframe) {
|
if (iframe) {
|
||||||
if (iframe.src.indexOf('?') !== -1) {
|
const iframeUrl = new URL(iframe.src)
|
||||||
iframe.src += '&';
|
|
||||||
} else {
|
iframeUrl.searchParams.set('autoplay', 1)
|
||||||
iframe.src += '?';
|
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,
|
// 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
|
// 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 () {
|
renderVideo () {
|
||||||
const { card } = this.props;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -421,6 +421,7 @@ export const DetailedStatus: React.FC<{
|
|||||||
<QuotedStatus
|
<QuotedStatus
|
||||||
quote={status.get('quote')}
|
quote={status.get('quote')}
|
||||||
parentQuotePostId={status.get('id')}
|
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>} */
|
/** @type {ImmutableMap<string, import('flavours/glitch/models/status').Status>} */
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap();
|
||||||
@@ -75,11 +79,10 @@ export default function statuses(state = initialState, action) {
|
|||||||
return state.setIn([action.id, 'isLoading'], true);
|
return state.setIn([action.id, 'isLoading'], true);
|
||||||
case STATUS_FETCH_FAIL: {
|
case STATUS_FETCH_FAIL: {
|
||||||
if (action.parentQuotePostId && action.error.status === 404) {
|
if (action.parentQuotePostId && action.error.status === 404) {
|
||||||
return state
|
return removeStatusStub(state, action.id)
|
||||||
.delete(action.id)
|
|
||||||
.setIn([action.parentQuotePostId, 'quote', 'state'], 'deleted')
|
.setIn([action.parentQuotePostId, 'quote', 'state'], 'deleted')
|
||||||
} else {
|
} else {
|
||||||
return state.delete(action.id);
|
return removeStatusStub(state, action.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case STATUS_IMPORT:
|
case STATUS_IMPORT:
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ export function fetchStatus(id, {
|
|||||||
dispatch(fetchStatusSuccess(skipLoading));
|
dispatch(fetchStatusSuccess(skipLoading));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
|
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 domParser = new DOMParser();
|
||||||
|
|
||||||
const addAutoPlay = html => {
|
const handleIframeUrl = (html, url, providerName) => {
|
||||||
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
||||||
const iframe = document.querySelector('iframe');
|
const iframe = document.querySelector('iframe');
|
||||||
|
const startTime = new URL(url).searchParams.get('t')
|
||||||
|
|
||||||
if (iframe) {
|
if (iframe) {
|
||||||
if (iframe.src.indexOf('?') !== -1) {
|
const iframeUrl = new URL(iframe.src)
|
||||||
iframe.src += '&';
|
|
||||||
} else {
|
iframeUrl.searchParams.set('autoplay', 1)
|
||||||
iframe.src += '?';
|
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,
|
// 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
|
// 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 () {
|
renderVideo () {
|
||||||
const { card } = this.props;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -384,6 +384,7 @@ export const DetailedStatus: React.FC<{
|
|||||||
<QuotedStatus
|
<QuotedStatus
|
||||||
quote={status.get('quote')}
|
quote={status.get('quote')}
|
||||||
parentQuotePostId={status.get('id')}
|
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>} */
|
/** @type {ImmutableMap<string, import('mastodon/models/status').Status>} */
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap();
|
||||||
@@ -75,11 +79,10 @@ export default function statuses(state = initialState, action) {
|
|||||||
return state.setIn([action.id, 'isLoading'], true);
|
return state.setIn([action.id, 'isLoading'], true);
|
||||||
case STATUS_FETCH_FAIL: {
|
case STATUS_FETCH_FAIL: {
|
||||||
if (action.parentQuotePostId && action.error.status === 404) {
|
if (action.parentQuotePostId && action.error.status === 404) {
|
||||||
return state
|
return removeStatusStub(state, action.id)
|
||||||
.delete(action.id)
|
|
||||||
.setIn([action.parentQuotePostId, 'quote', 'state'], 'deleted')
|
.setIn([action.parentQuotePostId, 'quote', 'state'], 'deleted')
|
||||||
} else {
|
} else {
|
||||||
return state.delete(action.id);
|
return removeStatusStub(state, action.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case STATUS_IMPORT:
|
case STATUS_IMPORT:
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ActivityPub::Activity::Update < ActivityPub::Activity
|
class ActivityPub::Activity::Update < ActivityPub::Activity
|
||||||
|
# Updates to unknown objects older than that are ignored
|
||||||
|
OBJECT_AGE_THRESHOLD = 1.day
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@account.schedule_refresh_if_stale!
|
@account.schedule_refresh_if_stale!
|
||||||
|
|
||||||
@@ -28,6 +31,9 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
|
|||||||
|
|
||||||
@status = Status.find_by(uri: object_uri, account_id: @account.id)
|
@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
|
||||||
|
return if @status.nil? && object_too_old?
|
||||||
|
|
||||||
# We may be getting `Create` and `Update` out of order
|
# We may be getting `Create` and `Update` out of order
|
||||||
@status ||= ActivityPub::Activity::Create.new(@json, @account, **@options).perform
|
@status ||= ActivityPub::Activity::Create.new(@json, @account, **@options).perform
|
||||||
|
|
||||||
@@ -35,4 +41,10 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
|
|||||||
|
|
||||||
ActivityPub::ProcessStatusUpdateService.new.call(@status, @json, @object, request_id: @options[:request_id])
|
ActivityPub::ProcessStatusUpdateService.new.call(@status, @json, @object, request_id: @options[:request_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def object_too_old?
|
||||||
|
@object['published'].present? && @object['published'].to_datetime < OBJECT_AGE_THRESHOLD.ago
|
||||||
|
rescue Date::Error
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -112,10 +112,12 @@ class AttachmentBatch
|
|||||||
keys.each_slice(LIMIT) do |keys_slice|
|
keys.each_slice(LIMIT) do |keys_slice|
|
||||||
logger.debug { "Deleting #{keys_slice.size} objects" }
|
logger.debug { "Deleting #{keys_slice.size} objects" }
|
||||||
|
|
||||||
|
with_overridden_timeout(bucket.client, 120) do
|
||||||
bucket.delete_objects(delete: {
|
bucket.delete_objects(delete: {
|
||||||
objects: keys_slice.map { |key| { key: key } },
|
objects: keys_slice.map { |key| { key: key } },
|
||||||
quiet: true,
|
quiet: true,
|
||||||
})
|
})
|
||||||
|
end
|
||||||
rescue => e
|
rescue => e
|
||||||
retries += 1
|
retries += 1
|
||||||
|
|
||||||
@@ -134,6 +136,20 @@ class AttachmentBatch
|
|||||||
@bucket ||= records.first.public_send(@attachment_names.first).s3_bucket
|
@bucket ||= records.first.public_send(@attachment_names.first).s3_bucket
|
||||||
end
|
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
|
def nullified_attributes
|
||||||
@attachment_names.flat_map { |attachment_name| NULLABLE_ATTRIBUTES.map { |attribute| "#{attachment_name}_#{attribute}" } & klass.column_names }.index_with(nil)
|
@attachment_names.flat_map { |attachment_name| NULLABLE_ATTRIBUTES.map { |attribute| "#{attachment_name}_#{attribute}" } & klass.column_names }.index_with(nil)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,12 +26,7 @@ class StatusCacheHydrator
|
|||||||
|
|
||||||
def hydrate_non_reblog_payload(empty_payload, account_id, nested: false)
|
def hydrate_non_reblog_payload(empty_payload, account_id, nested: false)
|
||||||
empty_payload.tap do |payload|
|
empty_payload.tap do |payload|
|
||||||
fill_status_payload(payload, @status, account_id, nested:)
|
fill_status_payload(payload, @status, account_id, fresh: !nested, nested:)
|
||||||
|
|
||||||
if payload[:poll]
|
|
||||||
payload[:poll][:voted] = @status.account_id == account_id
|
|
||||||
payload[:poll][:own_votes] = []
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -45,18 +40,7 @@ class StatusCacheHydrator
|
|||||||
# used to create the status, we need to hydrate it here too
|
# 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
|
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:)
|
fill_status_payload(payload[:reblog], @status.reblog, account_id, fresh: false, 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
|
|
||||||
|
|
||||||
payload[:filtered] = payload[:reblog][:filtered]
|
payload[:filtered] = payload[:reblog][:filtered]
|
||||||
payload[:favourited] = payload[:reblog][:favourited]
|
payload[:favourited] = payload[:reblog][:favourited]
|
||||||
@@ -64,7 +48,7 @@ class StatusCacheHydrator
|
|||||||
end
|
end
|
||||||
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[: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[: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)
|
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[: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[:filtered] = mapped_applied_custom_filter(account_id, status)
|
||||||
payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account_id, nested:) if payload[:quote]
|
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
|
end
|
||||||
|
|
||||||
def hydrate_quote_payload(empty_payload, quote, account_id, nested: false)
|
def hydrate_quote_payload(empty_payload, quote, account_id, nested: false)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ services:
|
|||||||
|
|
||||||
# es:
|
# es:
|
||||||
# restart: always
|
# restart: always
|
||||||
# image: docker.elastic.co/elasticsearch/elasticsearch:7.17.4
|
# image: docker.elastic.co/elasticsearch/elasticsearch:7.17.29
|
||||||
# environment:
|
# environment:
|
||||||
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m -Des.enforce.bootstrap.checks=true"
|
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m -Des.enforce.bootstrap.checks=true"
|
||||||
# - "xpack.license.self_generated.type=basic"
|
# - "xpack.license.self_generated.type=basic"
|
||||||
@@ -59,7 +59,7 @@ services:
|
|||||||
web:
|
web:
|
||||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||||
# build: .
|
# build: .
|
||||||
image: ghcr.io/glitch-soc/mastodon:v4.4.8
|
image: ghcr.io/glitch-soc/mastodon:v4.4.10
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec puma -C config/puma.rb
|
command: bundle exec puma -C config/puma.rb
|
||||||
@@ -83,7 +83,7 @@ services:
|
|||||||
# build:
|
# build:
|
||||||
# dockerfile: ./streaming/Dockerfile
|
# dockerfile: ./streaming/Dockerfile
|
||||||
# context: .
|
# context: .
|
||||||
image: ghcr.io/glitch-soc/mastodon-streaming:v4.4.8
|
image: ghcr.io/glitch-soc/mastodon-streaming:v4.4.10
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: node ./streaming/index.js
|
command: node ./streaming/index.js
|
||||||
@@ -102,7 +102,7 @@ services:
|
|||||||
sidekiq:
|
sidekiq:
|
||||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||||
# build: .
|
# build: .
|
||||||
image: ghcr.io/glitch-soc/mastodon:v4.4.8
|
image: ghcr.io/glitch-soc/mastodon:v4.4.10
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
|
|||||||
@@ -123,12 +123,12 @@ module Mastodon::CLI
|
|||||||
progress.log("Moving #{previous_path} to #{upgraded_path}") if options[:verbose]
|
progress.log("Moving #{previous_path} to #{upgraded_path}") if options[:verbose]
|
||||||
|
|
||||||
begin
|
begin
|
||||||
move_previous_to_upgraded
|
move_previous_to_upgraded(previous_path, upgraded_path)
|
||||||
rescue => e
|
rescue => e
|
||||||
progress.log(pastel.red("Error processing #{previous_path}: #{e}"))
|
progress.log(pastel.red("Error processing #{previous_path}: #{e}"))
|
||||||
success = false
|
success = false
|
||||||
|
|
||||||
remove_directory
|
remove_directory(upgraded_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module Mastodon
|
|||||||
end
|
end
|
||||||
|
|
||||||
def patch
|
def patch
|
||||||
8
|
10
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_prerelease
|
def default_prerelease
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
"http-link-header": "^1.1.1",
|
"http-link-header": "^1.1.1",
|
||||||
"immutable": "^4.3.0",
|
"immutable": "^4.3.0",
|
||||||
"intl-messageformat": "^10.7.16",
|
"intl-messageformat": "^10.7.16",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.1",
|
||||||
"lande": "^1.0.10",
|
"lande": "^1.0.10",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marky": "^1.2.5",
|
"marky": "^1.2.5",
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ RSpec.describe ActivityPub::Activity do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
let(:publication_date) { 1.hour.ago.utc }
|
||||||
|
|
||||||
let(:create_json) do
|
let(:create_json) do
|
||||||
{
|
{
|
||||||
'@context': [
|
'@context': [
|
||||||
@@ -52,7 +54,7 @@ RSpec.describe ActivityPub::Activity do
|
|||||||
'https://www.w3.org/ns/activitystreams#Public',
|
'https://www.w3.org/ns/activitystreams#Public',
|
||||||
],
|
],
|
||||||
content: 'foo',
|
content: 'foo',
|
||||||
published: '2025-05-24T11:03:10Z',
|
published: publication_date.iso8601,
|
||||||
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||||
},
|
},
|
||||||
}.deep_stringify_keys
|
}.deep_stringify_keys
|
||||||
@@ -77,7 +79,7 @@ RSpec.describe ActivityPub::Activity do
|
|||||||
'https://www.w3.org/ns/activitystreams#Public',
|
'https://www.w3.org/ns/activitystreams#Public',
|
||||||
],
|
],
|
||||||
content: 'foo',
|
content: 'foo',
|
||||||
published: '2025-05-24T11:03:10Z',
|
published: publication_date.iso8601,
|
||||||
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||||
quoteAuthorization: approval_uri,
|
quoteAuthorization: approval_uri,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -164,6 +164,28 @@ RSpec.describe StatusCacheHydrator do
|
|||||||
end
|
end
|
||||||
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
|
context 'when the quoted post matches account filters' do
|
||||||
let(:quoted_status) { Fabricate(:status, text: 'this toot is about that banned word') }
|
let(:quoted_status) { Fabricate(:status, text: 'this toot is about that banned word') }
|
||||||
|
|
||||||
|
|||||||
19
yarn.lock
19
yarn.lock
@@ -2689,7 +2689,7 @@ __metadata:
|
|||||||
husky: "npm:^9.0.11"
|
husky: "npm:^9.0.11"
|
||||||
immutable: "npm:^4.3.0"
|
immutable: "npm:^4.3.0"
|
||||||
intl-messageformat: "npm:^10.7.16"
|
intl-messageformat: "npm:^10.7.16"
|
||||||
js-yaml: "npm:^4.1.0"
|
js-yaml: "npm:^4.1.1"
|
||||||
lande: "npm:^1.0.10"
|
lande: "npm:^1.0.10"
|
||||||
lint-staged: "npm:^16.0.0"
|
lint-staged: "npm:^16.0.0"
|
||||||
lodash: "npm:^4.17.21"
|
lodash: "npm:^4.17.21"
|
||||||
@@ -7769,8 +7769,8 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.4.1":
|
"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.4.1":
|
||||||
version: 10.4.5
|
version: 10.5.0
|
||||||
resolution: "glob@npm:10.4.5"
|
resolution: "glob@npm:10.5.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
foreground-child: "npm:^3.1.0"
|
foreground-child: "npm:^3.1.0"
|
||||||
jackspeak: "npm:^3.1.2"
|
jackspeak: "npm:^3.1.2"
|
||||||
@@ -7780,7 +7780,7 @@ __metadata:
|
|||||||
path-scurry: "npm:^1.11.1"
|
path-scurry: "npm:^1.11.1"
|
||||||
bin:
|
bin:
|
||||||
glob: dist/esm/bin.mjs
|
glob: dist/esm/bin.mjs
|
||||||
checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e
|
checksum: 10c0/100705eddbde6323e7b35e1d1ac28bcb58322095bd8e63a7d0bef1a2cdafe0d0f7922a981b2b48369a4f8c1b077be5c171804534c3509dfe950dde15fbe6d828
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -8762,6 +8762,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"jsdoc-type-pratt-parser@npm:~4.1.0":
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
resolution: "jsdoc-type-pratt-parser@npm:4.1.0"
|
resolution: "jsdoc-type-pratt-parser@npm:4.1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user