Merge pull request #3619 from glitch-soc/glitch-soc/merge-4.6
Merge upstream changes up to 1440d55b13 into stable-4.6
This commit is contained in:
20 files changed
+237
-105
No files matched your search
@@ -2,6 +2,16 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.6.5] - 2026-08-06
|
||||
|
||||
- Fix Collection items being rejected because of incorrect attribute being read (#40052 by @shleeable)
|
||||
- Fix typo in embedded quote handling code (#40049 by @shleeable)
|
||||
- Fix account merging worker incorrectly merging `Appeal` and `AccountWarning` records (#39982 by @shleeable)
|
||||
- Fix off-by-one in handling of updated remote posts allowing up to 5 attachments (#39978 by @shleeable)
|
||||
- Fix collection items limit not being consistently applied (#39969 by @oneiros)
|
||||
- Fix oversized profile image crop uploads (#39958 by @sharlayan)
|
||||
- Fix emoji autocomplete sometimes suggesting emojis for earlier keystrokes (#39947 by @ChaosExAnima)
|
||||
|
||||
## [4.6.4] - 2026-07-27
|
||||
|
||||
### Security
|
||||
|
||||
@@ -20,6 +20,8 @@ import { updateTimeline } from './timelines';
|
||||
let fetchComposeSuggestionsAccountsController;
|
||||
/** @type {AbortController | undefined} */
|
||||
let fetchComposeSuggestionsTagsController;
|
||||
/** @type {AbortController | undefined} */
|
||||
let searchComposeSuggestionsEmojiController;
|
||||
|
||||
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
||||
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
|
||||
@@ -531,9 +533,8 @@ export function undoUploadCompose(media_id) {
|
||||
}
|
||||
|
||||
export function clearComposeSuggestions() {
|
||||
if (fetchComposeSuggestionsAccountsController) {
|
||||
fetchComposeSuggestionsAccountsController.abort();
|
||||
}
|
||||
fetchComposeSuggestionsAccountsController?.abort();
|
||||
searchComposeSuggestionsEmojiController?.abort();
|
||||
return {
|
||||
type: COMPOSE_SUGGESTIONS_CLEAR,
|
||||
};
|
||||
@@ -566,12 +567,25 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, token) => {
|
||||
});
|
||||
}, 200, { leading: true, trailing: true });
|
||||
|
||||
const fetchComposeSuggestionsEmojis = async (dispatch, token) => {
|
||||
// Right now we are hard-coding the locale to English since the picker search only supports English.
|
||||
// Once we replace the legacy picker we can remove this and use the actual locale of the user.
|
||||
const results = await emojiMartSearch(token, 'en', 5);
|
||||
dispatch(readyComposeSuggestionsEmojis(token, results));
|
||||
};
|
||||
const fetchComposeSuggestionsEmojis = (dispatch, token) => {
|
||||
dispatch(clearComposeSuggestions());
|
||||
searchComposeSuggestionsEmojiController = new AbortController();
|
||||
|
||||
void emojiMartSearch({
|
||||
token,
|
||||
// Right now we are hard-coding the locale to English since the picker search only supports English.
|
||||
// Once we replace the legacy picker we can remove this and use the actual locale of the user.
|
||||
locale: 'en',
|
||||
limit: 5,
|
||||
signal: searchComposeSuggestionsEmojiController.signal,
|
||||
}).then((results) => {
|
||||
if (results) {
|
||||
dispatch(readyComposeSuggestionsEmojis(token, results));
|
||||
}
|
||||
}).finally(() => {
|
||||
searchComposeSuggestionsEmojiController = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
const fetchComposeSuggestionsTags = throttle((dispatch, token) => {
|
||||
if (fetchComposeSuggestionsTagsController) {
|
||||
|
||||
@@ -94,12 +94,12 @@ export const ImageUploadModal: FC<
|
||||
setStep('select');
|
||||
return;
|
||||
}
|
||||
void calculateCroppedImage(imageSrc, crop).then((blob) => {
|
||||
void calculateCroppedImage(imageSrc, crop, location).then((blob) => {
|
||||
setImageBlob(blob);
|
||||
setStep('alt');
|
||||
});
|
||||
},
|
||||
[imageSrc],
|
||||
[imageSrc, location],
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -418,9 +418,14 @@ const StepAlt: FC<{
|
||||
async function calculateCroppedImage(
|
||||
imageSrc: string,
|
||||
crop: Area,
|
||||
location: ImageLocation,
|
||||
): Promise<Blob> {
|
||||
const image = await dataUriToImage(imageSrc);
|
||||
const canvas = new OffscreenCanvas(crop.width, crop.height);
|
||||
const maxWidth = location === 'avatar' ? 400 : 1500;
|
||||
const scale = Math.min(1, maxWidth / crop.width);
|
||||
const width = Math.round(crop.width * scale);
|
||||
const height = Math.round(crop.height * scale);
|
||||
const canvas = new OffscreenCanvas(width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Failed to get canvas context');
|
||||
@@ -437,8 +442,8 @@ async function calculateCroppedImage(
|
||||
crop.height,
|
||||
0,
|
||||
0,
|
||||
crop.width,
|
||||
crop.height,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
|
||||
return canvas.convertToBlob();
|
||||
|
||||
@@ -22,34 +22,50 @@ type LegacyEmoji =
|
||||
};
|
||||
|
||||
// Replicates the old legacy search function.
|
||||
export async function emojiMartSearch(
|
||||
token: string,
|
||||
locale: string,
|
||||
export async function emojiMartSearch({
|
||||
token,
|
||||
locale,
|
||||
limit = 5,
|
||||
): Promise<LegacyEmoji[]> {
|
||||
const query = token.replace(':', '').trim();
|
||||
if (!query.length) {
|
||||
return [];
|
||||
signal,
|
||||
}: {
|
||||
token: string;
|
||||
locale: string;
|
||||
limit?: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<LegacyEmoji[] | null> {
|
||||
try {
|
||||
const query = token.replace(':', '').trim();
|
||||
if (!query.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cacheKey = `${query}|${locale}|${limit}`;
|
||||
const cachedResult = searchCache.get(cacheKey);
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const results = await search({
|
||||
query,
|
||||
locale,
|
||||
limit,
|
||||
signal,
|
||||
});
|
||||
const legacyResults = results.map((emoji) =>
|
||||
'shortcode' in emoji
|
||||
? ({ id: emoji.shortcode, custom: true } as const)
|
||||
: {
|
||||
id: emoji.label.replaceAll(' ', '_').toLowerCase(),
|
||||
native: emoji.unicode,
|
||||
},
|
||||
);
|
||||
searchCache.set(cacheKey, legacyResults);
|
||||
|
||||
return legacyResults;
|
||||
} catch {
|
||||
log('aborted search for "%s"', token);
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheKey = `${query}|${locale}|${limit}`;
|
||||
const cachedResult = searchCache.get(cacheKey);
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const results = await search({ query, locale, limit });
|
||||
const legacyResults = results.map((emoji) =>
|
||||
'shortcode' in emoji
|
||||
? ({ id: emoji.shortcode, custom: true } as const)
|
||||
: {
|
||||
id: emoji.label.replaceAll(' ', '_').toLowerCase(),
|
||||
native: emoji.unicode,
|
||||
},
|
||||
);
|
||||
searchCache.set(cacheKey, legacyResults);
|
||||
|
||||
return legacyResults;
|
||||
}
|
||||
|
||||
const defaultCategories = [
|
||||
|
||||
@@ -56,13 +56,18 @@ export async function search({
|
||||
query: rawQuery,
|
||||
locale: localeString,
|
||||
limit = 0,
|
||||
signal,
|
||||
}: {
|
||||
query: string;
|
||||
locale: string;
|
||||
limit?: number;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
log('searching for "%s"', rawQuery);
|
||||
performance.mark('emoji-search-start');
|
||||
|
||||
signal?.throwIfAborted();
|
||||
|
||||
// Get the locale, and extract tokens from the query.
|
||||
const locale = toSupportedLocale(localeString);
|
||||
const segmenter = localeToSegmenter(locale);
|
||||
@@ -93,6 +98,7 @@ export async function search({
|
||||
locale,
|
||||
i === queryTokens.length - 1,
|
||||
);
|
||||
signal?.throwIfAborted();
|
||||
const resultMap: ScoreMap = new Map();
|
||||
const checkedSet = new Set<string>();
|
||||
|
||||
@@ -121,6 +127,7 @@ export async function search({
|
||||
}
|
||||
|
||||
// Score based on legacy shortcodes, using the higher score if there's a match.
|
||||
signal?.throwIfAborted();
|
||||
for (const shortcodeResult of shortcodeResults) {
|
||||
const emoji =
|
||||
resultMap.get(shortcodeResult.hexcode) ??
|
||||
@@ -183,7 +190,9 @@ export async function search({
|
||||
|
||||
// If there are no results, try a cursor-based custom emoji search instead.
|
||||
if (mixedResults.length === 0 || mixedResults.length < limit) {
|
||||
signal?.throwIfAborted();
|
||||
const customEmojisFound = await fullCustomSearch(query, allEmojiIds);
|
||||
signal?.throwIfAborted();
|
||||
if (customEmojisFound.length > 0) {
|
||||
log(
|
||||
'cursor search found %d results for "%s"',
|
||||
@@ -373,6 +382,8 @@ async function fullCustomSearch(query: string, existing = new Set<string>()) {
|
||||
|
||||
// First iterate over chunks of 1,000 custom emoji keys and find any matches.
|
||||
const chunkSize = 1_000;
|
||||
const maxIterations = 10;
|
||||
let index = 0;
|
||||
let lastKey: string | null = null;
|
||||
let keys: string[] = [];
|
||||
do {
|
||||
@@ -389,6 +400,10 @@ async function fullCustomSearch(query: string, existing = new Set<string>()) {
|
||||
foundEmojis.add(key);
|
||||
}
|
||||
}
|
||||
index++;
|
||||
if (index >= maxIterations) {
|
||||
break;
|
||||
}
|
||||
} while (keys.length === chunkSize);
|
||||
|
||||
// Next get the full emojis for all matches.
|
||||
|
||||
@@ -518,7 +518,7 @@ body > [data-popper-placement] {
|
||||
}
|
||||
|
||||
.autosuggest-account .account__avatar,
|
||||
.autosuggest-emoji img {
|
||||
.autosuggest-emoji .emojione {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
@@ -19,6 +19,8 @@ import { updateTimeline } from './timelines';
|
||||
let fetchComposeSuggestionsAccountsController;
|
||||
/** @type {AbortController | undefined} */
|
||||
let fetchComposeSuggestionsTagsController;
|
||||
/** @type {AbortController | undefined} */
|
||||
let searchComposeSuggestionsEmojiController;
|
||||
|
||||
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
||||
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
|
||||
@@ -499,9 +501,8 @@ export function undoUploadCompose(media_id) {
|
||||
}
|
||||
|
||||
export function clearComposeSuggestions() {
|
||||
if (fetchComposeSuggestionsAccountsController) {
|
||||
fetchComposeSuggestionsAccountsController.abort();
|
||||
}
|
||||
fetchComposeSuggestionsAccountsController?.abort();
|
||||
searchComposeSuggestionsEmojiController?.abort();
|
||||
return {
|
||||
type: COMPOSE_SUGGESTIONS_CLEAR,
|
||||
};
|
||||
@@ -534,12 +535,25 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, token) => {
|
||||
});
|
||||
}, 200, { leading: true, trailing: true });
|
||||
|
||||
const fetchComposeSuggestionsEmojis = async (dispatch, token) => {
|
||||
// Right now we are hard-coding the locale to English since the picker search only supports English.
|
||||
// Once we replace the legacy picker we can remove this and use the actual locale of the user.
|
||||
const results = await emojiMartSearch(token, 'en', 5);
|
||||
dispatch(readyComposeSuggestionsEmojis(token, results));
|
||||
};
|
||||
const fetchComposeSuggestionsEmojis = (dispatch, token) => {
|
||||
dispatch(clearComposeSuggestions());
|
||||
searchComposeSuggestionsEmojiController = new AbortController();
|
||||
|
||||
void emojiMartSearch({
|
||||
token,
|
||||
// Right now we are hard-coding the locale to English since the picker search only supports English.
|
||||
// Once we replace the legacy picker we can remove this and use the actual locale of the user.
|
||||
locale: 'en',
|
||||
limit: 5,
|
||||
signal: searchComposeSuggestionsEmojiController.signal,
|
||||
}).then((results) => {
|
||||
if (results) {
|
||||
dispatch(readyComposeSuggestionsEmojis(token, results));
|
||||
}
|
||||
}).finally(() => {
|
||||
searchComposeSuggestionsEmojiController = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
const fetchComposeSuggestionsTags = throttle((dispatch, token) => {
|
||||
if (fetchComposeSuggestionsTagsController) {
|
||||
|
||||
@@ -94,12 +94,12 @@ export const ImageUploadModal: FC<
|
||||
setStep('select');
|
||||
return;
|
||||
}
|
||||
void calculateCroppedImage(imageSrc, crop).then((blob) => {
|
||||
void calculateCroppedImage(imageSrc, crop, location).then((blob) => {
|
||||
setImageBlob(blob);
|
||||
setStep('alt');
|
||||
});
|
||||
},
|
||||
[imageSrc],
|
||||
[imageSrc, location],
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -418,9 +418,14 @@ const StepAlt: FC<{
|
||||
async function calculateCroppedImage(
|
||||
imageSrc: string,
|
||||
crop: Area,
|
||||
location: ImageLocation,
|
||||
): Promise<Blob> {
|
||||
const image = await dataUriToImage(imageSrc);
|
||||
const canvas = new OffscreenCanvas(crop.width, crop.height);
|
||||
const maxWidth = location === 'avatar' ? 400 : 1500;
|
||||
const scale = Math.min(1, maxWidth / crop.width);
|
||||
const width = Math.round(crop.width * scale);
|
||||
const height = Math.round(crop.height * scale);
|
||||
const canvas = new OffscreenCanvas(width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Failed to get canvas context');
|
||||
@@ -437,8 +442,8 @@ async function calculateCroppedImage(
|
||||
crop.height,
|
||||
0,
|
||||
0,
|
||||
crop.width,
|
||||
crop.height,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
|
||||
return canvas.convertToBlob();
|
||||
|
||||
@@ -22,34 +22,50 @@ type LegacyEmoji =
|
||||
};
|
||||
|
||||
// Replicates the old legacy search function.
|
||||
export async function emojiMartSearch(
|
||||
token: string,
|
||||
locale: string,
|
||||
export async function emojiMartSearch({
|
||||
token,
|
||||
locale,
|
||||
limit = 5,
|
||||
): Promise<LegacyEmoji[]> {
|
||||
const query = token.replace(':', '').trim();
|
||||
if (!query.length) {
|
||||
return [];
|
||||
signal,
|
||||
}: {
|
||||
token: string;
|
||||
locale: string;
|
||||
limit?: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<LegacyEmoji[] | null> {
|
||||
try {
|
||||
const query = token.replace(':', '').trim();
|
||||
if (!query.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cacheKey = `${query}|${locale}|${limit}`;
|
||||
const cachedResult = searchCache.get(cacheKey);
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const results = await search({
|
||||
query,
|
||||
locale,
|
||||
limit,
|
||||
signal,
|
||||
});
|
||||
const legacyResults = results.map((emoji) =>
|
||||
'shortcode' in emoji
|
||||
? ({ id: emoji.shortcode, custom: true } as const)
|
||||
: {
|
||||
id: emoji.label.replaceAll(' ', '_').toLowerCase(),
|
||||
native: emoji.unicode,
|
||||
},
|
||||
);
|
||||
searchCache.set(cacheKey, legacyResults);
|
||||
|
||||
return legacyResults;
|
||||
} catch {
|
||||
log('aborted search for "%s"', token);
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheKey = `${query}|${locale}|${limit}`;
|
||||
const cachedResult = searchCache.get(cacheKey);
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const results = await search({ query, locale, limit });
|
||||
const legacyResults = results.map((emoji) =>
|
||||
'shortcode' in emoji
|
||||
? ({ id: emoji.shortcode, custom: true } as const)
|
||||
: {
|
||||
id: emoji.label.replaceAll(' ', '_').toLowerCase(),
|
||||
native: emoji.unicode,
|
||||
},
|
||||
);
|
||||
searchCache.set(cacheKey, legacyResults);
|
||||
|
||||
return legacyResults;
|
||||
}
|
||||
|
||||
const defaultCategories = [
|
||||
|
||||
@@ -56,13 +56,18 @@ export async function search({
|
||||
query: rawQuery,
|
||||
locale: localeString,
|
||||
limit = 0,
|
||||
signal,
|
||||
}: {
|
||||
query: string;
|
||||
locale: string;
|
||||
limit?: number;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
log('searching for "%s"', rawQuery);
|
||||
performance.mark('emoji-search-start');
|
||||
|
||||
signal?.throwIfAborted();
|
||||
|
||||
// Get the locale, and extract tokens from the query.
|
||||
const locale = toSupportedLocale(localeString);
|
||||
const segmenter = localeToSegmenter(locale);
|
||||
@@ -93,6 +98,7 @@ export async function search({
|
||||
locale,
|
||||
i === queryTokens.length - 1,
|
||||
);
|
||||
signal?.throwIfAborted();
|
||||
const resultMap: ScoreMap = new Map();
|
||||
const checkedSet = new Set<string>();
|
||||
|
||||
@@ -121,6 +127,7 @@ export async function search({
|
||||
}
|
||||
|
||||
// Score based on legacy shortcodes, using the higher score if there's a match.
|
||||
signal?.throwIfAborted();
|
||||
for (const shortcodeResult of shortcodeResults) {
|
||||
const emoji =
|
||||
resultMap.get(shortcodeResult.hexcode) ??
|
||||
@@ -183,7 +190,9 @@ export async function search({
|
||||
|
||||
// If there are no results, try a cursor-based custom emoji search instead.
|
||||
if (mixedResults.length === 0 || mixedResults.length < limit) {
|
||||
signal?.throwIfAborted();
|
||||
const customEmojisFound = await fullCustomSearch(query, allEmojiIds);
|
||||
signal?.throwIfAborted();
|
||||
if (customEmojisFound.length > 0) {
|
||||
log(
|
||||
'cursor search found %d results for "%s"',
|
||||
@@ -373,6 +382,8 @@ async function fullCustomSearch(query: string, existing = new Set<string>()) {
|
||||
|
||||
// First iterate over chunks of 1,000 custom emoji keys and find any matches.
|
||||
const chunkSize = 1_000;
|
||||
const maxIterations = 10;
|
||||
let index = 0;
|
||||
let lastKey: string | null = null;
|
||||
let keys: string[] = [];
|
||||
do {
|
||||
@@ -389,6 +400,10 @@ async function fullCustomSearch(query: string, existing = new Set<string>()) {
|
||||
foundEmojis.add(key);
|
||||
}
|
||||
}
|
||||
index++;
|
||||
if (index >= maxIterations) {
|
||||
break;
|
||||
}
|
||||
} while (keys.length === chunkSize);
|
||||
|
||||
// Next get the full emojis for all matches.
|
||||
|
||||
@@ -518,7 +518,7 @@ body > [data-popper-placement] {
|
||||
}
|
||||
|
||||
.autosuggest-account .account__avatar,
|
||||
.autosuggest-emoji img {
|
||||
.autosuggest-emoji .emojione {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
@@ -404,7 +404,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
def fetch_and_verify_quote
|
||||
return if @quote.nil?
|
||||
|
||||
embedded_quote = safe_prefetched_embed(@account, @status_parser.quoted_object, @json['context'])
|
||||
embedded_quote = safe_prefetched_embed(@account, @status_parser.quoted_object, @json['@context'])
|
||||
ActivityPub::VerifyQuoteService.new.call(@quote, @quote_approval_uri, fetchable_quoted_uri: @quote_uri, prefetched_quoted_object: embedded_quote, request_id: @options[:request_id], depth: @options[:depth])
|
||||
rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||
ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(PROCESSING_DELAY), @quote.id, @quote_uri, { 'request_id' => @options[:request_id], 'approval_uri' => @quote_approval_uri })
|
||||
|
||||
@@ -54,7 +54,7 @@ class Collection < ApplicationRecord
|
||||
if: :remote?
|
||||
validates :language, language: { if: :local?, allow_nil: true }
|
||||
validate :tag_is_usable
|
||||
validate :items_do_not_exceed_limit
|
||||
validate :items_do_not_exceed_limit, if: :local?
|
||||
validate :user_does_not_exceed_limit, on: :create
|
||||
|
||||
scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) }
|
||||
|
||||
@@ -16,10 +16,10 @@ module Account::Merging
|
||||
Notification, NotificationPermission, NotificationRequest
|
||||
],
|
||||
target_account_id: [
|
||||
Follow, FollowRequest, Block, Mute, AccountModerationNote, AccountPin, AccountNote
|
||||
Follow, FollowRequest, Block, Mute, AccountModerationNote, AccountPin, AccountNote,
|
||||
AccountWarning
|
||||
],
|
||||
reference_account_id: [CanonicalEmailBlock],
|
||||
account_warning_id: [Appeal],
|
||||
local_account_id: [SeveredRelationship],
|
||||
remote_account_id: [SeveredRelationship],
|
||||
quoted_account_id: [Quote],
|
||||
|
||||
@@ -17,9 +17,10 @@ class ActivityPub::ProcessFeaturedItemService
|
||||
return if non_matching_actor_and_approval_uris?
|
||||
return if non_supported_object_type?
|
||||
|
||||
with_redis_lock("collection_item:#{@item_json['id']}") do
|
||||
@collection_item = existing_item || pre_approved_item || new_item
|
||||
@collection_item = existing_item || pre_approved_item || new_item
|
||||
return if @collection_item.nil?
|
||||
|
||||
with_redis_lock("collection_item:#{@item_json['id']}") do
|
||||
@collection_item.position = position unless position.nil?
|
||||
@collection_item.update!(
|
||||
uri: @item_json['id'],
|
||||
@@ -45,6 +46,8 @@ class ActivityPub::ProcessFeaturedItemService
|
||||
end
|
||||
|
||||
def new_item
|
||||
return if @collection.collection_items.count >= ActivityPub::ProcessFeaturedCollectionService::ITEMS_LIMIT
|
||||
|
||||
@collection.collection_items.new(
|
||||
created_at: @item_json['published']
|
||||
)
|
||||
@@ -67,7 +70,7 @@ class ActivityPub::ProcessFeaturedItemService
|
||||
return false if Account.exists?(uri: @actor_uri)
|
||||
|
||||
object_json = fetch_resource(@actor_uri, true, raise_on_error: :temporary)
|
||||
object_json.nil? || (as_array(object_json['fetch']) & ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES).empty?
|
||||
object_json.nil? || (as_array(object_json['type']) & ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES).empty?
|
||||
end
|
||||
|
||||
def verify_authorization!
|
||||
|
||||
@@ -92,7 +92,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
as_array(@json['attachment']).each do |attachment|
|
||||
media_attachment_parser = ActivityPub::Parser::MediaAttachmentParser.new(attachment)
|
||||
|
||||
next if media_attachment_parser.remote_url.blank? || @next_media_attachments.size > Status::MEDIA_ATTACHMENTS_LIMIT
|
||||
next if media_attachment_parser.remote_url.blank? || @next_media_attachments.size >= Status::MEDIA_ATTACHMENTS_LIMIT
|
||||
|
||||
begin
|
||||
media_attachment = previous_media_attachments.find { |previous_media_attachment| previous_media_attachment.remote_url == media_attachment_parser.remote_url }
|
||||
@@ -377,7 +377,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
end
|
||||
|
||||
def fetch_and_verify_quote!(quote, approval_uri, quote_uri)
|
||||
embedded_quote = safe_prefetched_embed(@account, @status_parser.quoted_object, @activity_json['context'])
|
||||
embedded_quote = safe_prefetched_embed(@account, @status_parser.quoted_object, @activity_json['@context'])
|
||||
ActivityPub::VerifyQuoteService.new.call(quote, approval_uri, fetchable_quoted_uri: quote_uri, prefetched_quoted_object: embedded_quote, request_id: @request_id)
|
||||
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||
ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(PROCESSING_DELAY), quote.id, quote_uri, { 'request_id' => @request_id, 'approval_uri' => approval_uri })
|
||||
|
||||
+3
-3
@@ -60,7 +60,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.6.4
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.6.5
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec puma -C config/puma.rb
|
||||
@@ -84,7 +84,7 @@ services:
|
||||
# build:
|
||||
# dockerfile: ./streaming/Dockerfile
|
||||
# context: .
|
||||
image: ghcr.io/glitch-soc/mastodon-streaming:v4.6.4
|
||||
image: ghcr.io/glitch-soc/mastodon-streaming:v4.6.5
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: node ./streaming/index.js
|
||||
@@ -103,7 +103,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.6.4
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.6.5
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec sidekiq
|
||||
|
||||
@@ -13,7 +13,7 @@ module Mastodon
|
||||
end
|
||||
|
||||
def patch
|
||||
4
|
||||
5
|
||||
end
|
||||
|
||||
def default_prerelease
|
||||
|
||||
@@ -53,20 +53,28 @@ RSpec.describe Collection do
|
||||
end
|
||||
|
||||
context 'when there are more items than allowed' do
|
||||
subject { Fabricate.build(:collection, collection_items:) }
|
||||
|
||||
let(:collection_items) { Fabricate.build_times(described_class::MAX_ITEMS + 1, :collection_item, collection: nil) }
|
||||
|
||||
it { is_expected.to_not be_valid }
|
||||
context 'when collection is local' do
|
||||
subject { Fabricate.build(:collection, collection_items:) }
|
||||
|
||||
context 'when the limit is only exceeded due to `rejected` and `revoked` items' do
|
||||
let(:collection_items) do
|
||||
items = Fabricate.build_times(described_class::MAX_ITEMS - 2, :collection_item, collection: nil, state: :accepted)
|
||||
items << Fabricate.build(:collection_item, collection: nil, state: :pending)
|
||||
items << Fabricate.build(:collection_item, collection: nil, state: :rejected)
|
||||
items << Fabricate.build(:collection_item, collection: nil, state: :revoked)
|
||||
items
|
||||
it { is_expected.to_not be_valid }
|
||||
|
||||
context 'when the limit is only exceeded due to `rejected` and `revoked` items' do
|
||||
let(:collection_items) do
|
||||
items = Fabricate.build_times(described_class::MAX_ITEMS - 2, :collection_item, collection: nil, state: :accepted)
|
||||
items << Fabricate.build(:collection_item, collection: nil, state: :pending)
|
||||
items << Fabricate.build(:collection_item, collection: nil, state: :rejected)
|
||||
items << Fabricate.build(:collection_item, collection: nil, state: :revoked)
|
||||
items
|
||||
end
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when collection is remote' do
|
||||
subject { Fabricate.build(:remote_collection, collection_items:) }
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
@@ -81,6 +81,17 @@ RSpec.describe ActivityPub::ProcessFeaturedItemService do
|
||||
expect(new_item.position).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context "when the collection's item count is already at the limit" do
|
||||
before do
|
||||
stub_const('ActivityPub::ProcessFeaturedCollectionService::ITEMS_LIMIT', 3)
|
||||
Fabricate.times(3, :collection_item, collection:)
|
||||
end
|
||||
|
||||
it 'does not create an item' do
|
||||
expect { subject.call(collection, object) }.to_not change(collection.collection_items, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when item exists' do
|
||||
|
||||
Reference in new issue
Block a user