From 21c27eb3affc486767334cf1b1431e4398fcafc0 Mon Sep 17 00:00:00 2001 From: Echo Date: Mon, 16 Mar 2026 12:39:52 +0100 Subject: [PATCH 01/24] Profile editing: Uploading avatar and header images (#38189) --- .../mastodon/actions/compose_typed.ts | 4 + app/javascript/mastodon/api/accounts.ts | 2 +- .../components/character_counter/index.tsx | 2 + .../account_edit/components/image_edit.tsx | 35 +- .../account_edit/modals/image_alt.tsx | 3 +- .../account_edit/modals/image_delete.tsx | 3 +- .../account_edit/modals/image_upload.tsx | 484 +++++++++++++++++- .../account_edit/modals/styles.module.scss | 49 ++ app/javascript/mastodon/features/ui/index.jsx | 17 +- app/javascript/mastodon/locales/en.json | 14 + app/javascript/mastodon/reducers/compose.js | 5 + .../mastodon/reducers/slices/profile_edit.ts | 46 ++ package.json | 1 + yarn.lock | 21 + 14 files changed, 647 insertions(+), 39 deletions(-) diff --git a/app/javascript/mastodon/actions/compose_typed.ts b/app/javascript/mastodon/actions/compose_typed.ts index 6b38b25c25..6bf193ba92 100644 --- a/app/javascript/mastodon/actions/compose_typed.ts +++ b/app/javascript/mastodon/actions/compose_typed.ts @@ -273,3 +273,7 @@ export const quoteComposeCancel = createAction('compose/quoteComposeCancel'); export const setComposeQuotePolicy = createAction( 'compose/setQuotePolicy', ); + +export const setDragUploadEnabled = createAction( + 'compose/setDragUploadEnabled', +); diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index da4b0e94f8..15156e156c 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -67,5 +67,5 @@ export const apiGetFamiliarFollowers = (id: string) => export const apiGetProfile = () => apiRequestGet('v1/profile'); -export const apiPatchProfile = (params: ApiProfileUpdateParams) => +export const apiPatchProfile = (params: ApiProfileUpdateParams | FormData) => apiRequestPatch('v1/profile', params); diff --git a/app/javascript/mastodon/components/character_counter/index.tsx b/app/javascript/mastodon/components/character_counter/index.tsx index dce410a7c1..6ffe4d02f4 100644 --- a/app/javascript/mastodon/components/character_counter/index.tsx +++ b/app/javascript/mastodon/components/character_counter/index.tsx @@ -26,6 +26,7 @@ export const CharacterCounter = polymorphicForwardRef< maxLength, as: Component = 'span', recommended = false, + className, ...props }, ref, @@ -39,6 +40,7 @@ export const CharacterCounter = polymorphicForwardRef< {...props} ref={ref} className={classNames( + className, classes.counter, currentLength > maxLength && !recommended && classes.counterError, )} diff --git a/app/javascript/mastodon/features/account_edit/components/image_edit.tsx b/app/javascript/mastodon/features/account_edit/components/image_edit.tsx index b99b424aec..340b8156eb 100644 --- a/app/javascript/mastodon/features/account_edit/components/image_edit.tsx +++ b/app/javascript/mastodon/features/account_edit/components/image_edit.tsx @@ -12,11 +12,9 @@ import { openModal } from '@/mastodon/actions/modal'; import { Dropdown } from '@/mastodon/components/dropdown_menu'; import { IconButton } from '@/mastodon/components/icon_button'; import type { MenuItem } from '@/mastodon/models/dropdown_menu'; -import { - createAppSelector, - useAppDispatch, - useAppSelector, -} from '@/mastodon/store'; +import type { ImageLocation } from '@/mastodon/reducers/slices/profile_edit'; +import { selectImageInfo } from '@/mastodon/reducers/slices/profile_edit'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import AddIcon from '@/material-icons/400-24px/add.svg?react'; import DeleteIcon from '@/material-icons/400-24px/delete.svg?react'; import EditIcon from '@/material-icons/400-24px/edit.svg?react'; @@ -50,36 +48,15 @@ const messages = defineMessages({ }, }); -export type ImageLocation = 'avatar' | 'header'; - -const selectImageInfo = createAppSelector( - [ - (state) => state.profileEdit.profile, - (_, location: ImageLocation) => location, - ], - (profile, location) => { - if (!profile) { - return { - hasImage: false, - hasAlt: false, - }; - } - - return { - hasImage: !!profile[`${location}Static`], - hasAlt: !!profile[`${location}Description`], - }; - }, -); - export const AccountImageEdit: FC<{ className?: string; location: ImageLocation; }> = ({ className, location }) => { const intl = useIntl(); - const { hasAlt, hasImage } = useAppSelector((state) => + const { alt, src } = useAppSelector((state) => selectImageInfo(state, location), ); + const hasAlt = !!alt; const dispatch = useAppDispatch(); const handleModal = useCallback( @@ -125,7 +102,7 @@ export const AccountImageEdit: FC<{ const iconClassName = classNames(classes.imageButton, className); - if (!hasImage) { + if (!src) { return ( = ({ onClose }) => { - return ; +> = ({ onClose, location }) => { + const { src: oldSrc } = useAppSelector((state) => + selectImageInfo(state, location), + ); + const hasImage = !!oldSrc; + const [step, setStep] = useState<'select' | 'crop' | 'alt'>('select'); + + // State for individual steps. + const [imageSrc, setImageSrc] = useState(null); + const [imageBlob, setImageBlob] = useState(null); + + const handleFile = useCallback((file: File) => { + const reader = new FileReader(); + reader.addEventListener('load', () => { + const result = reader.result; + if (typeof result === 'string' && result.length > 0) { + setImageSrc(result); + setStep('crop'); + } + }); + reader.readAsDataURL(file); + }, []); + + const handleCrop = useCallback( + (crop: Area) => { + if (!imageSrc) { + setStep('select'); + return; + } + void calculateCroppedImage(imageSrc, crop).then((blob) => { + setImageBlob(blob); + setStep('alt'); + }); + }, + [imageSrc], + ); + + const dispatch = useAppDispatch(); + const handleSave = useCallback( + (altText: string) => { + if (!imageBlob) { + setStep('crop'); + return; + } + void dispatch(uploadImage({ location, imageBlob, altText })).then( + onClose, + ); + }, + [dispatch, imageBlob, location, onClose], + ); + + const handleCancel = useCallback(() => { + switch (step) { + case 'crop': + setImageSrc(null); + setStep('select'); + break; + case 'alt': + setImageBlob(null); + setStep('crop'); + break; + default: + onClose(); + } + }, [onClose, step]); + + return ( + + ) : ( + + ) + } + onClose={onClose} + wrapperClassName={classes.uploadWrapper} + noCancelButton + > + {step === 'select' && ( + + )} + {step === 'crop' && imageSrc && ( + + )} + {step === 'alt' && imageBlob && ( + + )} + + ); }; + +// Taken from app/models/concerns/account/header.rb and app/models/concerns/account/avatar.rb +const ALLOWED_MIME_TYPES = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', +]; + +const StepUpload: FC<{ + location: ImageLocation; + onFile: (file: File) => void; +}> = ({ location, onFile }) => { + const inputRef = useRef(null); + const handleUploadClick = useCallback(() => { + inputRef.current?.click(); + }, []); + + const handleFileChange: ChangeEventHandler = useCallback( + (event) => { + const file = event.currentTarget.files?.[0]; + if (!file || !ALLOWED_MIME_TYPES.includes(file.type)) { + return; + } + onFile(file); + }, + [onFile], + ); + + // Handle drag and drop + const [isDragging, setDragging] = useState(false); + + const handleDragOver = useCallback((event: DragEvent) => { + event.preventDefault(); + if (!event.dataTransfer?.types.includes('Files')) { + return; + } + + const items = Array.from(event.dataTransfer.items); + if ( + !items.some( + (item) => + item.kind === 'file' && ALLOWED_MIME_TYPES.includes(item.type), + ) + ) { + return; + } + + setDragging(true); + }, []); + const handleDragDrop = useCallback( + (event: DragEvent) => { + event.preventDefault(); + setDragging(false); + + if (!event.dataTransfer?.files) { + return; + } + + const file = Array.from(event.dataTransfer.files).find((f) => + ALLOWED_MIME_TYPES.includes(f.type), + ); + if (!file) { + return; + } + + onFile(file); + }, + [onFile], + ); + const handleDragLeave = useCallback((event: DragEvent) => { + event.preventDefault(); + setDragging(false); + }, []); + + const dispatch = useAppDispatch(); + useEffect(() => { + dispatch(setDragUploadEnabled(false)); + document.addEventListener('dragover', handleDragOver); + document.addEventListener('drop', handleDragDrop); + document.addEventListener('dragleave', handleDragLeave); + + return () => { + document.removeEventListener('dragover', handleDragOver); + document.removeEventListener('drop', handleDragDrop); + document.removeEventListener('dragleave', handleDragLeave); + dispatch(setDragUploadEnabled(true)); + }; + }, [handleDragLeave, handleDragDrop, handleDragOver, dispatch]); + + if (isDragging) { + return ( +
+ +
+ ); + } + + return ( +
+ + , + limit: 8, + width: location === 'avatar' ? 400 : 1500, + height: location === 'avatar' ? 400 : 500, + }} + tagName='p' + /> + + + +
+ ); +}; + +const zoomLabel = defineMessage({ + id: 'account_edit.upload_modal.step_crop.zoom', + defaultMessage: 'Zoom', +}); + +const StepCrop: FC<{ + src: string; + location: ImageLocation; + onCancel: () => void; + onComplete: (crop: Area) => void; +}> = ({ src, location, onCancel, onComplete }) => { + const [crop, setCrop] = useState({ x: 0, y: 0 }); + const [croppedArea, setCroppedArea] = useState(null); + const [zoom, setZoom] = useState(1); + const intl = useIntl(); + + const handleZoomChange: ChangeEventHandler = useCallback( + (event) => { + setZoom(event.currentTarget.valueAsNumber); + }, + [], + ); + const handleCropComplete = useCallback((_: Area, croppedAreaPixels: Area) => { + setCroppedArea(croppedAreaPixels); + }, []); + + const handleNext = useCallback(() => { + if (croppedArea) { + onComplete(croppedArea); + } + }, [croppedArea, onComplete]); + + return ( + <> +
+ +
+ +
+ + + +
+ + ); +}; + +const StepAlt: FC<{ + imageBlob: Blob; + onCancel: () => void; + onComplete: (altText: string) => void; +}> = ({ imageBlob, onCancel, onComplete }) => { + const [altText, setAltText] = useState(''); + + const handleChange: ChangeEventHandler = useCallback( + (event) => { + setAltText(event.currentTarget.value); + }, + [], + ); + + const handleComplete = useCallback(() => { + onComplete(altText); + }, [altText, onComplete]); + + const imageSrc = useMemo(() => URL.createObjectURL(imageBlob), [imageBlob]); + const altLimit = useAppSelector( + (state) => + state.server.getIn( + ['server', 'configuration', 'media_attachments', 'description_limit'], + 150, + ) as number, + ); + + return ( + <> + + +
+ + } + hint={ + + } + onChange={handleChange} + /> + +
+ + + } + > + + + +
+ + + +
+ + ); +}; + +async function calculateCroppedImage( + imageSrc: string, + crop: Area, +): Promise { + const image = await dataUriToImage(imageSrc); + const canvas = new OffscreenCanvas(crop.width, crop.height); + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Failed to get canvas context'); + } + + ctx.imageSmoothingQuality = 'high'; + + // Draw the image + ctx.drawImage( + image, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height, + ); + + return canvas.convertToBlob({ + quality: 0.7, + type: 'image/jpeg', + }); +} + +function dataUriToImage(dataUri: string) { + return new Promise((resolve, reject) => { + const image = new Image(); + image.addEventListener('load', () => { + resolve(image); + }); + image.addEventListener('error', (event) => { + if (event.error instanceof Error) { + reject(event.error); + } else { + reject(new Error('Failed to load image')); + } + }); + image.src = dataUri; + }); +} diff --git a/app/javascript/mastodon/features/account_edit/modals/styles.module.scss b/app/javascript/mastodon/features/account_edit/modals/styles.module.scss index 0bd4c07a15..0a0a956eb5 100644 --- a/app/javascript/mastodon/features/account_edit/modals/styles.module.scss +++ b/app/javascript/mastodon/features/account_edit/modals/styles.module.scss @@ -80,6 +80,55 @@ } } +.uploadWrapper { + min-height: min(400px, 70vh); + justify-content: center; +} + +.uploadStepSelect { + text-align: center; + + h2 { + color: var(--color-text-primary); + font-size: 15px; + font-weight: 600; + margin-bottom: 4px; + } + + button { + margin-top: 16px; + } +} + +.cropContainer { + position: relative; + width: 100%; + height: 300px; + overflow: hidden; +} + +.cropActions { + margin-top: 8px; // 16px gap from DialogModal, plus 8px = 24px to look like normal action buttons. + display: flex; + gap: 8px; + align-items: center; + justify-content: flex-end; + + .zoomControl { + width: min(100%, 200px); + margin-right: auto; + } +} + +.altImage { + max-height: 300px; + object-fit: contain; +} + +.altCounter { + color: var(--color-text-secondary); +} + .verifiedSteps { font-size: 15px; diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 3e14b016e9..eae6d35a5f 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -103,7 +103,11 @@ const mapStateToProps = state => ({ layout: state.getIn(['meta', 'layout']), isComposing: state.getIn(['compose', 'is_composing']), hasComposingContents: state.getIn(['compose', 'text']).trim().length !== 0 || state.getIn(['compose', 'media_attachments']).size > 0 || state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'quoted_status_id']) !== null, - canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']), + canUploadMore: + !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) + && state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']), + isUploadEnabled: + state.getIn(['compose', 'isDragDisabled']) !== true, firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION, newAccount: !state.getIn(['accounts', me, 'note']) && !state.getIn(['accounts', me, 'bot']) && state.getIn(['accounts', me, 'following_count'], 0) === 0 && state.getIn(['accounts', me, 'statuses_count'], 0) === 0, username: state.getIn(['accounts', me, 'username']), @@ -324,6 +328,9 @@ class UI extends PureComponent { }; handleDragEnter = (e) => { + if (!this.props.isUploadEnabled) { + return; + } e.preventDefault(); if (!this.dragTargets) { @@ -340,6 +347,9 @@ class UI extends PureComponent { }; handleDragOver = (e) => { + if (!this.props.isUploadEnabled) { + return; + } if (this.dataTransferIsText(e.dataTransfer)) return false; e.preventDefault(); @@ -355,6 +365,9 @@ class UI extends PureComponent { }; handleDrop = (e) => { + if (!this.props.isUploadEnabled) { + return; + } if (this.dataTransferIsText(e.dataTransfer)) return; e.preventDefault(); @@ -429,7 +442,6 @@ class UI extends PureComponent { document.addEventListener('dragover', this.handleDragOver, false); document.addEventListener('drop', this.handleDrop, false); document.addEventListener('dragleave', this.handleDragLeave, false); - document.addEventListener('dragend', this.handleDragEnd, false); if ('serviceWorker' in navigator) { navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage); @@ -456,7 +468,6 @@ class UI extends PureComponent { document.removeEventListener('dragover', this.handleDragOver); document.removeEventListener('drop', this.handleDrop); document.removeEventListener('dragleave', this.handleDragLeave); - document.removeEventListener('dragend', this.handleDragEnd); } setRef = c => { diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index db5c1313a5..26ce99c3da 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -203,6 +203,20 @@ "account_edit.profile_tab.subtitle": "Customize the tabs on your profile and what they display.", "account_edit.profile_tab.title": "Profile tab settings", "account_edit.save": "Save", + "account_edit.upload_modal.back": "Back", + "account_edit.upload_modal.done": "Done", + "account_edit.upload_modal.next": "Next", + "account_edit.upload_modal.step_alt.callout_text": "Adding alt text to media helps people using screen readers to understand your content.", + "account_edit.upload_modal.step_alt.callout_title": "Let’s make Mastodon accessible for all", + "account_edit.upload_modal.step_alt.text_hint": "E.g. “Close-up photo of me wearing glasses and a blue shirt”", + "account_edit.upload_modal.step_alt.text_label": "Alt text", + "account_edit.upload_modal.step_crop.zoom": "Zoom", + "account_edit.upload_modal.step_upload.button": "Browse files", + "account_edit.upload_modal.step_upload.dragging": "Drop to upload", + "account_edit.upload_modal.step_upload.header": "Choose an image", + "account_edit.upload_modal.step_upload.hint": "WEBP, PNG, GIF or JPG format, up to {limit}MB.{br}Image will be scaled to {width}x{height}px.", + "account_edit.upload_modal.title_add": "Add profile photo", + "account_edit.upload_modal.title_replace": "Replace profile photo", "account_edit.verified_modal.details": "Add credibility to your Mastodon profile by verifying links to personal websites. Here’s how it works:", "account_edit.verified_modal.invisible_link.details": "Add the link to your header. The important part is rel=\"me\" which prevents impersonation on websites with user-generated content. You can even use a link tag in the header of the page instead of {tag}, but the HTML must be accessible without executing JavaScript.", "account_edit.verified_modal.invisible_link.summary": "How do I make the link invisible?", diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 51508c777d..705b3186ba 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -8,6 +8,7 @@ import { setComposeQuotePolicy, pasteLinkCompose, cancelPasteLinkCompose, + setDragUploadEnabled, } from '@/mastodon/actions/compose_typed'; import { timelineDelete } from 'mastodon/actions/timelines_typed'; @@ -75,6 +76,7 @@ const initialState = ImmutableMap({ is_submitting: false, is_changing_upload: false, is_uploading: false, + isDragDisabled: false, should_redirect_to_compose_page: false, progress: 0, isUploadingThumbnail: false, @@ -132,6 +134,7 @@ function clearAll(state) { map.set('idempotencyKey', uuid()); map.set('quoted_status_id', null); map.set('quote_policy', state.get('default_quote_policy')); + map.set('isDragDisabled', false); }); } @@ -359,6 +362,8 @@ export const composeReducer = (state = initialState, action) => { return action.meta.requestId === state.get('fetching_link') ? state.set('fetching_link', null) : state; } else if (cancelPasteLinkCompose.match(action)) { return state.set('fetching_link', null); + } else if (setDragUploadEnabled.match(action)) { + return state.set('isDragDisabled', !action.payload); } switch(action.type) { diff --git a/app/javascript/mastodon/reducers/slices/profile_edit.ts b/app/javascript/mastodon/reducers/slices/profile_edit.ts index 62a908e5b1..7efd71eb3e 100644 --- a/app/javascript/mastodon/reducers/slices/profile_edit.ts +++ b/app/javascript/mastodon/reducers/slices/profile_edit.ts @@ -109,6 +109,17 @@ const profileEditSlice = createSlice({ state.isPending = false; }); + builder.addCase(uploadImage.pending, (state) => { + state.isPending = true; + }); + builder.addCase(uploadImage.rejected, (state) => { + state.isPending = false; + }); + builder.addCase(uploadImage.fulfilled, (state, action) => { + state.profile = action.payload; + state.isPending = false; + }); + builder.addCase(addFeaturedTag.pending, (state) => { state.isPending = true; }); @@ -229,6 +240,41 @@ export const patchProfile = createDataLoadingThunk( }, ); +export type ImageLocation = 'avatar' | 'header'; + +export const selectImageInfo = createAppSelector( + [ + (state) => state.profileEdit.profile, + (_, location: ImageLocation) => location, + ], + (profile, location) => { + if (!profile) { + return {}; + } + + return { + src: profile[location], + static: profile[`${location}Static`], + alt: profile[`${location}Description`], + }; + }, +); + +export const uploadImage = createDataLoadingThunk( + `${profileEditSlice.name}/uploadImage`, + (arg: { location: ImageLocation; imageBlob: Blob; altText: string }) => { + // Note: Alt text is not actually supported by the API yet. + const formData = new FormData(); + formData.append(arg.location, arg.imageBlob); + + return apiPatchProfile(formData); + }, + transformProfile, + { + useLoadingBar: false, + }, +); + export const selectFieldById = createAppSelector( [(state) => state.profileEdit.profile?.fields, (_, id?: string) => id], (fields, fieldId) => { diff --git a/package.json b/package.json index e196a7c596..9ab025be45 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "punycode": "^2.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-easy-crop": "^5.5.6", "react-helmet": "^6.1.0", "react-immutable-proptypes": "^2.2.0", "react-immutable-pure-component": "^2.2.2", diff --git a/yarn.lock b/yarn.lock index d99c4b2aa5..2795e9bd97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2919,6 +2919,7 @@ __metadata: punycode: "npm:^2.3.0" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" + react-easy-crop: "npm:^5.5.6" react-helmet: "npm:^6.1.0" react-immutable-proptypes: "npm:^2.2.0" react-immutable-pure-component: "npm:^2.2.2" @@ -10244,6 +10245,13 @@ __metadata: languageName: node linkType: hard +"normalize-wheel@npm:^1.0.1": + version: 1.0.1 + resolution: "normalize-wheel@npm:1.0.1" + checksum: 10c0/5daf4c97e39f36658a5263a6499bbc148676ae2bd85f12c8d03c46ffe7bc3c68d44564c00413d88d0457ac0d94450559bb1c24c2ce7ae0c107031f82d093ac06 + languageName: node + linkType: hard + "object-assign@npm:^4, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -11703,6 +11711,19 @@ __metadata: languageName: node linkType: hard +"react-easy-crop@npm:^5.5.6": + version: 5.5.6 + resolution: "react-easy-crop@npm:5.5.6" + dependencies: + normalize-wheel: "npm:^1.0.1" + tslib: "npm:^2.0.1" + peerDependencies: + react: ">=16.4.0" + react-dom: ">=16.4.0" + checksum: 10c0/ce623791d31559fc46f210ece7b22c0f659710d5de219ef9fb05650940f50445d5e6573ed229b66fad06dfda9651ae458c0f5efb8e1cabdf01511dc32942cdc8 + languageName: node + linkType: hard + "react-fast-compare@npm:^3.1.1": version: 3.2.2 resolution: "react-fast-compare@npm:3.2.2" From f9b2dffaa8bb7d6451eb9b3c3766299094dbad86 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 09:55:53 -0400 Subject: [PATCH 02/24] Use `JSON.generate` call in push update worker (#38208) --- app/workers/push_update_worker.rb | 6 +++--- spec/workers/push_update_worker_spec.rb | 13 +++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/workers/push_update_worker.rb b/app/workers/push_update_worker.rb index c32975a986..63240d50b0 100644 --- a/app/workers/push_update_worker.rb +++ b/app/workers/push_update_worker.rb @@ -23,10 +23,10 @@ class PushUpdateWorker end def message - Oj.dump( + JSON.generate({ event: update? ? :'status.update' : :update, - payload: @payload - ) + payload: @payload, + }.as_json) end def publish! diff --git a/spec/workers/push_update_worker_spec.rb b/spec/workers/push_update_worker_spec.rb index f3e0a128df..a423031fb7 100644 --- a/spec/workers/push_update_worker_spec.rb +++ b/spec/workers/push_update_worker_spec.rb @@ -15,7 +15,7 @@ RSpec.describe PushUpdateWorker do context 'with valid records' do let(:account) { Fabricate :account } - let(:status) { Fabricate :status } + let(:status) { Fabricate :status, text: 'Test Post' } before { allow(redis).to receive(:publish) } @@ -25,7 +25,16 @@ RSpec.describe PushUpdateWorker do expect(redis) .to have_received(:publish) - .with(redis_key, anything) + .with( + redis_key, + match_json_values( + event: 'update', + payload: include( + created_at: status.created_at.iso8601(3), + content: eq('

Test Post

') + ) + ) + ) end def redis_key From 60442197469edc61bc9b6ecb96cd5e7dbc47b576 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 09:55:58 -0400 Subject: [PATCH 03/24] Use `to_json` call for raw event strings (#38215) --- app/lib/access_token_extension.rb | 2 +- app/lib/application_extension.rb | 2 +- app/lib/feed_manager.rb | 4 ++-- app/models/concerns/account/suspensions.rb | 2 +- app/models/custom_filter.rb | 4 ++-- app/models/user.rb | 4 ++-- app/services/batched_remove_status_service.rb | 2 +- app/services/notify_service.rb | 2 +- app/services/remove_status_service.rb | 2 +- app/workers/publish_announcement_reaction_worker.rb | 2 +- app/workers/publish_scheduled_announcement_worker.rb | 2 +- app/workers/push_conversation_worker.rb | 2 +- app/workers/unfilter_notifications_worker.rb | 2 +- app/workers/unpublish_announcement_worker.rb | 2 +- spec/lib/feed_manager_spec.rb | 2 +- spec/models/user_spec.rb | 4 ++-- spec/requests/api/v2/filters_spec.rb | 2 +- spec/services/remove_status_service_spec.rb | 2 +- 18 files changed, 22 insertions(+), 22 deletions(-) diff --git a/app/lib/access_token_extension.rb b/app/lib/access_token_extension.rb index 6e06f988a5..268232a436 100644 --- a/app/lib/access_token_extension.rb +++ b/app/lib/access_token_extension.rb @@ -24,6 +24,6 @@ module AccessTokenExtension end def push_to_streaming_api - redis.publish("timeline:access_token:#{id}", Oj.dump(event: :kill)) if revoked? || destroyed? + redis.publish("timeline:access_token:#{id}", { event: :kill }.to_json) if revoked? || destroyed? end end diff --git a/app/lib/application_extension.rb b/app/lib/application_extension.rb index bc6c7561cc..b8906d339b 100644 --- a/app/lib/application_extension.rb +++ b/app/lib/application_extension.rb @@ -35,7 +35,7 @@ module ApplicationExtension def close_streaming_sessions(resource_owner = nil) # TODO: #28793 Combine into a single topic - payload = Oj.dump(event: :kill) + payload = { event: :kill }.to_json scope = access_tokens scope = scope.where(resource_owner_id: resource_owner.id) unless resource_owner.nil? scope.in_batches do |tokens| diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 18a58156c3..444b96c7ce 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -90,7 +90,7 @@ class FeedManager def unpush_from_home(account, status, update: false) return false unless remove_from_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?) - redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update + redis.publish("timeline:#{account.id}", { event: :delete, payload: status.id.to_s }.to_json) unless update true end @@ -117,7 +117,7 @@ class FeedManager def unpush_from_list(list, status, update: false) return false unless remove_from_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?) - redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update + redis.publish("timeline:list:#{list.id}", { event: :delete, payload: status.id.to_s }.to_json) unless update true end diff --git a/app/models/concerns/account/suspensions.rb b/app/models/concerns/account/suspensions.rb index 4c9ca593ad..28c6bb8c66 100644 --- a/app/models/concerns/account/suspensions.rb +++ b/app/models/concerns/account/suspensions.rb @@ -35,7 +35,7 @@ module Account::Suspensions # This terminates all connections for the given account with the streaming # server: - redis.publish("timeline:system:#{id}", Oj.dump(event: :kill)) if local? + redis.publish("timeline:system:#{id}", { event: :kill }.to_json) if local? end def unsuspend! diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb index 1151c7de98..a5d8e937e3 100644 --- a/app/models/custom_filter.rb +++ b/app/models/custom_filter.rb @@ -115,8 +115,8 @@ class CustomFilter < ApplicationRecord @should_invalidate_cache = false Rails.cache.delete("filters:v3:#{account_id}") - redis.publish("timeline:#{account_id}", Oj.dump(event: :filters_changed)) - redis.publish("timeline:system:#{account_id}", Oj.dump(event: :filters_changed)) + redis.publish("timeline:#{account_id}", { event: :filters_changed }.to_json) + redis.publish("timeline:system:#{account_id}", { event: :filters_changed }.to_json) end private diff --git a/app/models/user.rb b/app/models/user.rb index dd029d8e08..a774d8953a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -173,7 +173,7 @@ class User < ApplicationRecord # This terminates all connections for the given account with the streaming # server: - redis.publish("timeline:system:#{account.id}", Oj.dump(event: :kill)) + redis.publish("timeline:system:#{account.id}", { event: :kill }.to_json) end def enable! @@ -347,7 +347,7 @@ class User < ApplicationRecord # Revoke each access token for the Streaming API, since `update_all`` # doesn't trigger ActiveRecord Callbacks: # TODO: #28793 Combine into a single topic - payload = Oj.dump(event: :kill) + payload = { event: :kill }.to_json redis.pipelined do |pipeline| batch.ids.each do |id| pipeline.publish("timeline:access_token:#{id}", payload) diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 826dbcc720..4dad80fc11 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -82,7 +82,7 @@ class BatchedRemoveStatusService < BaseService def unpush_from_public_timelines(status, pipeline) return unless status.public_visibility? && status.id > @status_id_cutoff - payload = Oj.dump(event: :delete, payload: status.id.to_s) + payload = { event: :delete, payload: status.id.to_s }.to_json pipeline.publish('timeline:public', payload) pipeline.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload) diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 2f009d5a23..ed292736d8 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -259,7 +259,7 @@ class NotifyService < BaseService end def push_to_streaming_api! - redis.publish("timeline:#{@recipient.id}:notifications", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification))) + redis.publish("timeline:#{@recipient.id}:notifications", { event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification) }.to_json) end def subscribed_to_streaming_api? diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index caa22b4729..042a20ec79 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -14,7 +14,7 @@ class RemoveStatusService < BaseService # @option [Boolean] :original_removed # @option [Boolean] :skip_streaming def call(status, **options) - @payload = Oj.dump(event: :delete, payload: status.id.to_s) + @payload = { event: :delete, payload: status.id.to_s }.to_json @status = status @account = status.account @options = options diff --git a/app/workers/publish_announcement_reaction_worker.rb b/app/workers/publish_announcement_reaction_worker.rb index 03da56550a..7cf7393c1d 100644 --- a/app/workers/publish_announcement_reaction_worker.rb +++ b/app/workers/publish_announcement_reaction_worker.rb @@ -11,7 +11,7 @@ class PublishAnnouncementReactionWorker reaction ||= announcement.announcement_reactions.new(name: name) payload = InlineRenderer.render(reaction, nil, :reaction).tap { |h| h[:announcement_id] = announcement_id.to_s } - payload = Oj.dump(event: :'announcement.reaction', payload: payload) + payload = { event: :'announcement.reaction', payload: payload } FeedManager.instance.with_active_accounts do |account| redis.publish("timeline:#{account.id}", payload) if redis.exists?("subscribed:timeline:#{account.id}") diff --git a/app/workers/publish_scheduled_announcement_worker.rb b/app/workers/publish_scheduled_announcement_worker.rb index c23eae6af7..63f1600d34 100644 --- a/app/workers/publish_scheduled_announcement_worker.rb +++ b/app/workers/publish_scheduled_announcement_worker.rb @@ -12,7 +12,7 @@ class PublishScheduledAnnouncementWorker @announcement.publish! unless @announcement.published? payload = InlineRenderer.render(@announcement, nil, :announcement) - payload = Oj.dump(event: :announcement, payload: payload) + payload = { event: :announcement, payload: payload }.to_json FeedManager.instance.with_active_accounts do |account| redis.publish("timeline:#{account.id}", payload) if redis.exists?("subscribed:timeline:#{account.id}") diff --git a/app/workers/push_conversation_worker.rb b/app/workers/push_conversation_worker.rb index 23b1469f11..b3990c1479 100644 --- a/app/workers/push_conversation_worker.rb +++ b/app/workers/push_conversation_worker.rb @@ -9,7 +9,7 @@ class PushConversationWorker message = InlineRenderer.render(conversation, conversation.account, :conversation) timeline_id = "timeline:direct:#{conversation.account_id}" - redis.publish(timeline_id, Oj.dump(event: :conversation, payload: message)) + redis.publish(timeline_id, { event: :conversation, payload: message }.to_json) rescue ActiveRecord::RecordNotFound true end diff --git a/app/workers/unfilter_notifications_worker.rb b/app/workers/unfilter_notifications_worker.rb index cb8a46b8f4..7b57a2db13 100644 --- a/app/workers/unfilter_notifications_worker.rb +++ b/app/workers/unfilter_notifications_worker.rb @@ -39,7 +39,7 @@ class UnfilterNotificationsWorker end def push_streaming_event! - redis.publish("timeline:#{@recipient.id}:notifications", Oj.dump(event: :notifications_merged, payload: '1')) + redis.publish("timeline:#{@recipient.id}:notifications", { event: :notifications_merged, payload: '1' }.to_json) end def subscribed_to_streaming_api? diff --git a/app/workers/unpublish_announcement_worker.rb b/app/workers/unpublish_announcement_worker.rb index e58c07554a..1b61bacb24 100644 --- a/app/workers/unpublish_announcement_worker.rb +++ b/app/workers/unpublish_announcement_worker.rb @@ -5,7 +5,7 @@ class UnpublishAnnouncementWorker include Redisable def perform(announcement_id) - payload = Oj.dump(event: :'announcement.delete', payload: announcement_id.to_s) + payload = { event: :'announcement.delete', payload: announcement_id.to_s }.to_json FeedManager.instance.with_active_accounts do |account| redis.publish("timeline:#{account.id}", payload) if redis.exists?("subscribed:timeline:#{account.id}") diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 0d0c817b6c..c8e44190bd 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -546,7 +546,7 @@ RSpec.describe FeedManager do allow(redis).to receive_messages(publish: nil) subject.unpush_from_home(receiver, status) - deletion = Oj.dump(event: :delete, payload: status.id.to_s) + deletion = { event: :delete, payload: status.id.to_s }.to_json expect(redis).to have_received(:publish).with("timeline:#{receiver.id}", deletion) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 187f05f02e..a7ac034f0a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -403,7 +403,7 @@ RSpec.describe User do expect(user).to have_attributes(disabled: true) expect(redis) - .to have_received(:publish).with("timeline:system:#{user.account.id}", Oj.dump(event: :kill)).once + .to have_received(:publish).with("timeline:system:#{user.account.id}", { event: :kill }.to_json).once end end @@ -445,7 +445,7 @@ RSpec.describe User do expect { web_push_subscription.reload } .to raise_error(ActiveRecord::RecordNotFound) expect(redis_pipeline_stub) - .to have_received(:publish).with("timeline:access_token:#{access_token.id}", Oj.dump(event: :kill)).once + .to have_received(:publish).with("timeline:access_token:#{access_token.id}", { event: :kill }.to_json).once end def remove_activated_sessions diff --git a/spec/requests/api/v2/filters_spec.rb b/spec/requests/api/v2/filters_spec.rb index cfa607cff0..4613d4f7b4 100644 --- a/spec/requests/api/v2/filters_spec.rb +++ b/spec/requests/api/v2/filters_spec.rb @@ -222,7 +222,7 @@ RSpec.describe 'Filters' do expect(keyword.reload.keyword).to eq 'updated' - expect(redis).to have_received(:publish).with("timeline:#{user.account.id}", Oj.dump(event: :filters_changed)).once + expect(redis).to have_received(:publish).with("timeline:#{user.account.id}", { event: :filters_changed }.to_json).once end end diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index 3cb2eceec5..91a902b733 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -40,7 +40,7 @@ RSpec.describe RemoveStatusService, :inline_jobs do .to_not include(status.id) expect(redis) - .to have_received(:publish).with('timeline:public:media', Oj.dump(event: :delete, payload: status.id.to_s)) + .to have_received(:publish).with('timeline:public:media', { event: :delete, payload: status.id.to_s }.to_json) expect(delete_delivery(hank, status)) .to have_been_made.once From 638429037f72d5b88ba85a069451f785af51549d Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 09:58:39 -0400 Subject: [PATCH 04/24] Use `to_json` call for libre translate api (#38216) --- app/lib/translation_service/libre_translate.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/translation_service/libre_translate.rb b/app/lib/translation_service/libre_translate.rb index 0df8590f87..a63cc723f1 100644 --- a/app/lib/translation_service/libre_translate.rb +++ b/app/lib/translation_service/libre_translate.rb @@ -9,7 +9,7 @@ class TranslationService::LibreTranslate < TranslationService end def translate(texts, source_language, target_language) - body = Oj.dump(q: texts, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key) + body = { q: texts, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key }.to_json request(:post, '/translate', body: body) do |res| transform_response(res.body_with_limit, source_language) end From 330357507de2a0f7fd30ca0ac21011ffec7b0088 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 10:01:37 -0400 Subject: [PATCH 05/24] Use `to_json` call for webhook service (#38217) --- app/services/webhook_service.rb | 4 +++- spec/services/webhook_service_spec.rb | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/services/webhook_service.rb b/app/services/webhook_service.rb index aafa383181..5441c754f3 100644 --- a/app/services/webhook_service.rb +++ b/app/services/webhook_service.rb @@ -17,6 +17,8 @@ class WebhookService < BaseService end def serialize_event - Oj.dump(ActiveModelSerializers::SerializableResource.new(@event, serializer: REST::Admin::WebhookEventSerializer, scope: nil, scope_name: :current_user).as_json) + ActiveModelSerializers::SerializableResource + .new(@event, serializer: REST::Admin::WebhookEventSerializer, scope: nil, scope_name: :current_user) + .to_json end end diff --git a/spec/services/webhook_service_spec.rb b/spec/services/webhook_service_spec.rb index 22a60db9f5..8c51542366 100644 --- a/spec/services/webhook_service_spec.rb +++ b/spec/services/webhook_service_spec.rb @@ -8,12 +8,14 @@ RSpec.describe WebhookService do let!(:report) { Fabricate(:report) } let!(:webhook) { Fabricate(:webhook, events: ['report.created']) } + before { freeze_time Time.current } + it 'finds and delivers webhook payloads' do expect { subject.call('report.created', report) } .to enqueue_sidekiq_job(Webhooks::DeliveryWorker) .with( webhook.id, - anything + match_json_values(event: 'report.created', created_at: Time.current.iso8601(3)) ) end end From 1a464bc5ededfad97e9a97d0c7f89f5a0a0ae493 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 10:02:52 -0400 Subject: [PATCH 06/24] Use `to_json` in simple view hash data-props build locations (#38218) --- app/views/shared/_web_app.html.haml | 2 +- app/views/shares/show.html.haml | 2 +- app/views/statuses/embed.html.haml | 2 +- spec/requests/statuses/embed_spec.rb | 2 ++ spec/system/home_spec.rb | 2 ++ spec/system/share_entrypoint_spec.rb | 2 ++ 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/views/shared/_web_app.html.haml b/app/views/shared/_web_app.html.haml index 5e6815165f..367d1c52d2 100644 --- a/app/views/shared/_web_app.html.haml +++ b/app/views/shared/_web_app.html.haml @@ -8,7 +8,7 @@ = render_initial_state = vite_typescript_tag 'application.ts', crossorigin: 'anonymous' -.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } } +.notranslate.app-holder#mastodon{ data: { props: default_props.to_json } } %noscript = image_tag frontend_asset_path('images/logo.svg'), alt: 'Mastodon' diff --git a/app/views/shares/show.html.haml b/app/views/shares/show.html.haml index c12b43031e..1d9717f1f8 100644 --- a/app/views/shares/show.html.haml +++ b/app/views/shares/show.html.haml @@ -2,4 +2,4 @@ = render_initial_state = vite_typescript_tag 'share.tsx', crossorigin: 'anonymous' -#mastodon-compose{ data: { props: Oj.dump(default_props) } } +#mastodon-compose{ data: { props: default_props.to_json } } diff --git a/app/views/statuses/embed.html.haml b/app/views/statuses/embed.html.haml index 09d0792ea2..74c807a89e 100644 --- a/app/views/statuses/embed.html.haml +++ b/app/views/statuses/embed.html.haml @@ -1 +1 @@ -#mastodon-status{ data: { props: Oj.dump(default_props.merge(id: @status.id.to_s)) } } +#mastodon-status{ data: { props: default_props.merge(id: @status.id.to_s).to_json } } diff --git a/spec/requests/statuses/embed_spec.rb b/spec/requests/statuses/embed_spec.rb index 33c7ea192c..7fc1b0125c 100644 --- a/spec/requests/statuses/embed_spec.rb +++ b/spec/requests/statuses/embed_spec.rb @@ -41,6 +41,8 @@ RSpec.describe 'Status embed' do .to have_http_status(200) expect(response.parsed_body.at('body.embed')) .to be_present + expect(response.parsed_body.at('#mastodon-status')['data-props']) + .to eq({ locale: 'en', id: status.id.to_s }.to_json) expect(response.headers).to include( 'Vary' => 'Accept, Accept-Language, Cookie', 'Cache-Control' => include('public'), diff --git a/spec/system/home_spec.rb b/spec/system/home_spec.rb index aafa9323c0..e839ae160b 100644 --- a/spec/system/home_spec.rb +++ b/spec/system/home_spec.rb @@ -12,6 +12,8 @@ RSpec.describe 'Home page' do expect(page) .to have_css('noscript', text: /Mastodon/) .and have_css('body', class: 'app-body') + expect(find('.app-holder#mastodon')['data-props']) + .to eq('{"locale":"en"}') end end diff --git a/spec/system/share_entrypoint_spec.rb b/spec/system/share_entrypoint_spec.rb index 0f07d96efe..8f91d28a12 100644 --- a/spec/system/share_entrypoint_spec.rb +++ b/spec/system/share_entrypoint_spec.rb @@ -19,6 +19,8 @@ RSpec.describe 'Share page', :js, :streaming do .to have_css('.modal-layout__mastodon') .and have_css('div#mastodon-compose') .and have_css('.compose-form__submit') + expect(find_by_id('mastodon-compose')['data-props']) + .to eq('{"locale":"en"}') fill_in_form From 8124f1581aa3770d7e61e920dfc8567192beae00 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 10:03:20 -0400 Subject: [PATCH 07/24] Use `to_json` call in cli/domains (#38219) --- lib/mastodon/cli/domains.rb | 2 +- spec/lib/mastodon/cli/domains_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mastodon/cli/domains.rb b/lib/mastodon/cli/domains.rb index c247463af5..cbd3465e04 100644 --- a/lib/mastodon/cli/domains.rb +++ b/lib/mastodon/cli/domains.rb @@ -214,7 +214,7 @@ module Mastodon::CLI def stats_to_json(stats) stats.compact! - say(Oj.dump(stats)) + say(stats.to_json) end end end diff --git a/spec/lib/mastodon/cli/domains_spec.rb b/spec/lib/mastodon/cli/domains_spec.rb index d1c26546f0..0d2c7f70a8 100644 --- a/spec/lib/mastodon/cli/domains_spec.rb +++ b/spec/lib/mastodon/cli/domains_spec.rb @@ -87,7 +87,7 @@ RSpec.describe Mastodon::CLI::Domains do end def json_summary - Oj.dump('host.example': { activity: {} }) + JSON.generate('host.example': { activity: {} }) end end end From b7246518bfc9953b2c507c2da0580d375bdfbc88 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 16 Mar 2026 15:04:25 +0100 Subject: [PATCH 08/24] Add `avatar_description` and `header_description` parameters to `PATCH /api/v1/profile` (#38221) --- app/controllers/api/v1/profiles_controller.rb | 2 ++ lib/mastodon/version.rb | 2 +- spec/requests/api/v1/profiles_spec.rb | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index 196d0ef3a7..02907f4fb4 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -25,7 +25,9 @@ class Api::V1::ProfilesController < Api::BaseController :display_name, :note, :avatar, + :avatar_description, :header, + :header_description, :locked, :bot, :discoverable, diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 76c849e8f6..e71f4d2d72 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -45,7 +45,7 @@ module Mastodon def api_versions { - mastodon: 8, + mastodon: 9, } end diff --git a/spec/requests/api/v1/profiles_spec.rb b/spec/requests/api/v1/profiles_spec.rb index b2c74b0191..7d0d1a3622 100644 --- a/spec/requests/api/v1/profiles_spec.rb +++ b/spec/requests/api/v1/profiles_spec.rb @@ -62,6 +62,7 @@ RSpec.describe 'Profile API' do let(:params) do { avatar: fixture_file_upload('avatar.gif', 'image/gif'), + avatar_description: 'animated walking round cat', discoverable: true, display_name: "Alice Isn't Dead", header: fixture_file_upload('attachment.jpg', 'image/jpeg'), @@ -110,6 +111,7 @@ RSpec.describe 'Profile API' do display_name: eq("Alice Isn't Dead"), note: 'Hello!', avatar: exist, + avatar_description: 'animated walking round cat', header: exist, attribution_domains: ['example.com'], fields: contain_exactly( From c05492ed5a549e483bb15f968402a2af98178477 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 10:31:58 -0400 Subject: [PATCH 09/24] Use `JSON.generate` call for fan out service (#38222) --- app/services/fan_out_on_write_service.rb | 6 +++--- spec/services/fan_out_on_write_service_spec.rb | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 727a6a63c4..d97e3e0fb2 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -171,10 +171,10 @@ class FanOutOnWriteService < BaseService end def anonymous_payload - @anonymous_payload ||= Oj.dump( + @anonymous_payload ||= JSON.generate({ event: update? ? :'status.update' : :update, - payload: rendered_status - ) + payload: rendered_status, + }.as_json) end def rendered_status diff --git a/spec/services/fan_out_on_write_service_spec.rb b/spec/services/fan_out_on_write_service_spec.rb index 79ecd06c8d..efc70d84c6 100644 --- a/spec/services/fan_out_on_write_service_spec.rb +++ b/spec/services/fan_out_on_write_service_spec.rb @@ -54,11 +54,18 @@ RSpec.describe FanOutOnWriteService do .and be_in(home_feed_of(bob)) .and be_in(home_feed_of(tom)) - expect(redis).to have_received(:publish).with('timeline:hashtag:hoge', anything) - expect(redis).to have_received(:publish).with('timeline:hashtag:hoge:local', anything) - expect(redis).to have_received(:publish).with('timeline:public', anything) - expect(redis).to have_received(:publish).with('timeline:public:local', anything) - expect(redis).to have_received(:publish).with('timeline:public:media', anything) + expected_payload = { event: 'update', payload: include(id: status.id.to_s, created_at: status.created_at.iso8601(3), content: /

Hello/) } + + expect(redis) + .to have_received(:publish).with('timeline:hashtag:hoge', match_json_values(expected_payload)) + expect(redis) + .to have_received(:publish).with('timeline:hashtag:hoge:local', match_json_values(expected_payload)) + expect(redis) + .to have_received(:publish).with('timeline:public', match_json_values(expected_payload)) + expect(redis) + .to have_received(:publish).with('timeline:public:local', match_json_values(expected_payload)) + expect(redis) + .to have_received(:publish).with('timeline:public:media', match_json_values(expected_payload)) end context 'with silenced_account_ids' do From 7933fa4f94fdfd5f09f1400313c223c93a73ac5c Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 10:32:05 -0400 Subject: [PATCH 10/24] Use `to_json` call in donation campaigns (#38223) --- app/controllers/api/v1/donation_campaigns_controller.rb | 2 +- spec/requests/api/v1/donation_campaigns_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/donation_campaigns_controller.rb b/app/controllers/api/v1/donation_campaigns_controller.rb index cdd7503b30..e1368e72f1 100644 --- a/app/controllers/api/v1/donation_campaigns_controller.rb +++ b/app/controllers/api/v1/donation_campaigns_controller.rb @@ -44,7 +44,7 @@ class Api::V1::DonationCampaignsController < Api::BaseController Rails.cache.write_multi( { request_key => campaign_key(campaign), - "donation_campaign:#{campaign_key(campaign)}" => Oj.dump(campaign), + "donation_campaign:#{campaign_key(campaign)}" => campaign.to_json, }, expires_in: 1.hour, raw: true diff --git a/spec/requests/api/v1/donation_campaigns_spec.rb b/spec/requests/api/v1/donation_campaigns_spec.rb index 2ab3fb8e8a..5df360b728 100644 --- a/spec/requests/api/v1/donation_campaigns_spec.rb +++ b/spec/requests/api/v1/donation_campaigns_spec.rb @@ -78,7 +78,7 @@ RSpec.describe 'Donation campaigns' do end before do - stub_request(:get, "#{api_url}?platform=web&seed=#{seed}&locale=en").to_return(body: Oj.dump(campaign_json), status: 200) + stub_request(:get, "#{api_url}?platform=web&seed=#{seed}&locale=en").to_return(body: JSON.generate(campaign_json), status: 200) end it 'returns the expected campaign' do From 8ed13bc6f7aa886058105066560dfa5143dfffeb Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 10:40:03 -0400 Subject: [PATCH 11/24] Use `to_json` call for accounts API (#38226) --- app/controllers/api/v1/accounts_controller.rb | 2 +- spec/requests/api/v1/accounts_spec.rb | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 936cd56eb8..8738da3c94 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -38,7 +38,7 @@ class Api::V1::AccountsController < Api::BaseController headers.merge!(response.headers) - self.response_body = Oj.dump(response.body) + self.response_body = response.body.to_json self.status = response.status rescue ActiveRecord::RecordInvalid => e render json: ValidationErrorFormatter.new(e, 'account.username': :username, 'invite_request.text': :reason).as_json, status: 422 diff --git a/spec/requests/api/v1/accounts_spec.rb b/spec/requests/api/v1/accounts_spec.rb index e3416fc337..0ea5c3921e 100644 --- a/spec/requests/api/v1/accounts_spec.rb +++ b/spec/requests/api/v1/accounts_spec.rb @@ -185,7 +185,12 @@ RSpec.describe '/api/v1/accounts' do expect(response).to have_http_status(200) expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body[:access_token]).to_not be_blank + expect(response.parsed_body) + .to include( + access_token: be_present, + created_at: be_a(Integer), + token_type: 'Bearer' + ) user = User.find_by(email: 'hello@world.tld') expect(user).to_not be_nil From 70230c632cdb74ede2fa141d7bf8323be8d387cd Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 10:40:51 -0400 Subject: [PATCH 12/24] Use `to_json` call for AP::Follow reject path (#38227) --- app/lib/activitypub/activity/follow.rb | 2 +- spec/lib/activitypub/activity/follow_spec.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index 97e41ab789..083b03cb7b 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -39,7 +39,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity end def reject_follow_request!(target_account) - json = Oj.dump(serialize_payload(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), ActivityPub::RejectFollowSerializer)) + json = serialize_payload(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), ActivityPub::RejectFollowSerializer).to_json ActivityPub::DeliveryWorker.perform_async(json, target_account.id, @account.inbox_url) end end diff --git a/spec/lib/activitypub/activity/follow_spec.rb b/spec/lib/activitypub/activity/follow_spec.rb index c1829cb8d7..3660a3914a 100644 --- a/spec/lib/activitypub/activity/follow_spec.rb +++ b/spec/lib/activitypub/activity/follow_spec.rb @@ -84,6 +84,23 @@ RSpec.describe ActivityPub::Activity::Follow do end end + context 'when recipient blocks sender' do + before { Fabricate :block, account: recipient, target_account: sender } + + it 'sends a reject and does not follow' do + subject.perform + + expect(sender.requested?(recipient)) + .to be false + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job( + match_json_values(type: 'Reject', object: include(type: 'Follow')), + recipient.id, + anything + ) + end + end + context 'when a follow relationship already exists' do before do sender.active_relationships.create!(target_account: recipient, uri: 'bar') From d9cd65f039ba078d7347f2d598f96dc5f2c543f7 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 10:49:09 -0400 Subject: [PATCH 13/24] Use `to_json` call for AP::QuoteRequest accept/reject paths (#38229) --- app/lib/activitypub/activity/quote_request.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/activitypub/activity/quote_request.rb b/app/lib/activitypub/activity/quote_request.rb index 46c45cde27..9e41bdec65 100644 --- a/app/lib/activitypub/activity/quote_request.rb +++ b/app/lib/activitypub/activity/quote_request.rb @@ -31,7 +31,7 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity status.quote.update!(activity_uri: @json['id']) status.quote.accept! - json = Oj.dump(serialize_payload(status.quote, ActivityPub::AcceptQuoteRequestSerializer)) + json = serialize_payload(status.quote, ActivityPub::AcceptQuoteRequestSerializer).to_json ActivityPub::DeliveryWorker.perform_async(json, quoted_status.account_id, @account.inbox_url) # Ensure the user is notified @@ -60,7 +60,7 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity account: @account, activity_uri: @json['id'] ) - json = Oj.dump(serialize_payload(quote, ActivityPub::RejectQuoteRequestSerializer)) + json = serialize_payload(quote, ActivityPub::RejectQuoteRequestSerializer).to_json ActivityPub::DeliveryWorker.perform_async(json, quoted_status.account_id, @account.inbox_url) end From 968ce25c39d0d7c7f7f5836f28443d5c3f600b7d Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 10:49:21 -0400 Subject: [PATCH 14/24] Use `to_json` call for worker payloads (#38228) --- app/workers/activitypub/distribute_poll_update_worker.rb | 2 +- app/workers/activitypub/distribution_worker.rb | 2 +- app/workers/activitypub/feature_request_worker.rb | 2 +- app/workers/activitypub/move_distribution_worker.rb | 2 +- app/workers/activitypub/quote_request_worker.rb | 2 +- app/workers/activitypub/update_distribution_worker.rb | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/workers/activitypub/distribute_poll_update_worker.rb b/app/workers/activitypub/distribute_poll_update_worker.rb index 8c1eefd93d..a2535a0ba2 100644 --- a/app/workers/activitypub/distribute_poll_update_worker.rb +++ b/app/workers/activitypub/distribute_poll_update_worker.rb @@ -43,7 +43,7 @@ class ActivityPub::DistributePollUpdateWorker end def payload - @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::UpdatePollSerializer, signer: @account)) + @payload ||= serialize_payload(@status, ActivityPub::UpdatePollSerializer, signer: @account).to_json end def relay! diff --git a/app/workers/activitypub/distribution_worker.rb b/app/workers/activitypub/distribution_worker.rb index 63013bdc69..a95de45183 100644 --- a/app/workers/activitypub/distribution_worker.rb +++ b/app/workers/activitypub/distribution_worker.rb @@ -24,7 +24,7 @@ class ActivityPub::DistributionWorker < ActivityPub::RawDistributionWorker end def payload - @payload ||= Oj.dump(serialize_payload(@status, activity_serializer, serializer_options.merge(signer: @account))) + @payload ||= serialize_payload(@status, activity_serializer, serializer_options.merge(signer: @account)).to_json end def activity_serializer diff --git a/app/workers/activitypub/feature_request_worker.rb b/app/workers/activitypub/feature_request_worker.rb index fa895a546d..61bc041f50 100644 --- a/app/workers/activitypub/feature_request_worker.rb +++ b/app/workers/activitypub/feature_request_worker.rb @@ -17,6 +17,6 @@ class ActivityPub::FeatureRequestWorker < ActivityPub::RawDistributionWorker end def payload - @payload ||= Oj.dump(serialize_payload(@collection_item, ActivityPub::FeatureRequestSerializer, signer: @account)) + @payload ||= serialize_payload(@collection_item, ActivityPub::FeatureRequestSerializer, signer: @account).to_json end end diff --git a/app/workers/activitypub/move_distribution_worker.rb b/app/workers/activitypub/move_distribution_worker.rb index 1680fcc76e..255a59cef0 100644 --- a/app/workers/activitypub/move_distribution_worker.rb +++ b/app/workers/activitypub/move_distribution_worker.rb @@ -28,6 +28,6 @@ class ActivityPub::MoveDistributionWorker end def signed_payload - @signed_payload ||= Oj.dump(serialize_payload(@migration, ActivityPub::MoveSerializer, signer: @account)) + @signed_payload ||= serialize_payload(@migration, ActivityPub::MoveSerializer, signer: @account).to_json end end diff --git a/app/workers/activitypub/quote_request_worker.rb b/app/workers/activitypub/quote_request_worker.rb index 0540492f86..45e328bb80 100644 --- a/app/workers/activitypub/quote_request_worker.rb +++ b/app/workers/activitypub/quote_request_worker.rb @@ -17,6 +17,6 @@ class ActivityPub::QuoteRequestWorker < ActivityPub::RawDistributionWorker end def payload - @payload ||= Oj.dump(serialize_payload(@quote, ActivityPub::QuoteRequestSerializer, signer: @account, allow_post_inlining: true)) + @payload ||= serialize_payload(@quote, ActivityPub::QuoteRequestSerializer, signer: @account, allow_post_inlining: true).to_json end end diff --git a/app/workers/activitypub/update_distribution_worker.rb b/app/workers/activitypub/update_distribution_worker.rb index 976f516498..6b6c905632 100644 --- a/app/workers/activitypub/update_distribution_worker.rb +++ b/app/workers/activitypub/update_distribution_worker.rb @@ -23,6 +23,6 @@ class ActivityPub::UpdateDistributionWorker < ActivityPub::RawDistributionWorker end def payload - @payload ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account, sign_with: @options[:sign_with])) + @payload ||= serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account, sign_with: @options[:sign_with]).to_json end end From 092acbd47b2da58defb8ac834d00dd861ce4855e Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 11:06:22 -0400 Subject: [PATCH 15/24] Use `to_json` call for pins API (#38231) --- app/controllers/api/v1/statuses/pins_controller.rb | 4 ++-- spec/requests/api/v1/statuses/pins_spec.rb | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb index 32a5f71293..39662eff73 100644 --- a/app/controllers/api/v1/statuses/pins_controller.rb +++ b/app/controllers/api/v1/statuses/pins_controller.rb @@ -30,7 +30,7 @@ class Api::V1::Statuses::PinsController < Api::V1::Statuses::BaseController adapter: ActivityPub::Adapter ).as_json - ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id) + ActivityPub::RawDistributionWorker.perform_async(json.to_json, current_account.id) end def distribute_remove_activity! @@ -40,6 +40,6 @@ class Api::V1::Statuses::PinsController < Api::V1::Statuses::BaseController adapter: ActivityPub::Adapter ).as_json - ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id) + ActivityPub::RawDistributionWorker.perform_async(json.to_json, current_account.id) end end diff --git a/spec/requests/api/v1/statuses/pins_spec.rb b/spec/requests/api/v1/statuses/pins_spec.rb index 66ed1510a4..26e939cd51 100644 --- a/spec/requests/api/v1/statuses/pins_spec.rb +++ b/spec/requests/api/v1/statuses/pins_spec.rb @@ -29,6 +29,8 @@ RSpec.describe 'Pins' do expect(response.parsed_body).to match( a_hash_including(id: status.id.to_s, pinned: true) ) + expect(ActivityPub::RawDistributionWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Add'), user.account.id) end end @@ -118,6 +120,8 @@ RSpec.describe 'Pins' do expect(response.parsed_body).to match( a_hash_including(id: status.id.to_s, pinned: false) ) + expect(ActivityPub::RawDistributionWorker) + .to have_enqueued_sidekiq_job(match_json_values(type: 'Remove'), user.account.id) end end From 6b1eac8865309d20336038575ab556a80c897b16 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 11:09:28 -0400 Subject: [PATCH 16/24] Use `to_json` call for Relay enable/disable (#38232) --- app/models/relay.rb | 4 ++-- spec/models/relay_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/relay.rb b/app/models/relay.rb index 53221887bd..41a0a2a1ae 100644 --- a/app/models/relay.rb +++ b/app/models/relay.rb @@ -31,7 +31,7 @@ class Relay < ApplicationRecord def enable! activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil) - payload = Oj.dump(follow_activity(activity_id)) + payload = follow_activity(activity_id).to_json update!(state: :pending, follow_activity_id: activity_id) reset_delivery_tracker @@ -40,7 +40,7 @@ class Relay < ApplicationRecord def disable! activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil) - payload = Oj.dump(unfollow_activity(activity_id)) + payload = unfollow_activity(activity_id).to_json update!(state: :idle, follow_activity_id: nil) reset_delivery_tracker diff --git a/spec/models/relay_spec.rb b/spec/models/relay_spec.rb index 03758ca6a8..e1fed60b81 100644 --- a/spec/models/relay_spec.rb +++ b/spec/models/relay_spec.rb @@ -78,7 +78,7 @@ RSpec.describe Relay do .to change { relay.reload.state }.to('idle') .and change { relay.reload.follow_activity_id }.to(be_nil) expect(ActivityPub::DeliveryWorker) - .to have_received(:perform_async).with(match('Undo'), Account.representative.id, relay.inbox_url) + .to have_received(:perform_async).with(match_json_values(type: 'Undo'), Account.representative.id, relay.inbox_url) expect(DeliveryFailureTracker) .to have_received(:reset!).with(relay.inbox_url) end @@ -94,7 +94,7 @@ RSpec.describe Relay do .to change { relay.reload.state }.to('pending') .and change { relay.reload.follow_activity_id }.to(be_present) expect(ActivityPub::DeliveryWorker) - .to have_received(:perform_async).with(match('Follow'), Account.representative.id, relay.inbox_url) + .to have_received(:perform_async).with(match_json_values(type: 'Follow'), Account.representative.id, relay.inbox_url) expect(DeliveryFailureTracker) .to have_received(:reset!).with(relay.inbox_url) end From 0c75e97345aff96689bcd5960ce48e459b47f318 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 11:10:10 -0400 Subject: [PATCH 17/24] Use `JSON.generate` in backup service (#38234) --- app/services/backup_service.rb | 14 +++++++------- spec/services/backup_service_spec.rb | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 299b5df234..bbe655b71a 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -23,7 +23,7 @@ class BackupService < BaseService skeleton = serialize(collection_presenter, ActivityPub::CollectionSerializer) skeleton[:@context] = full_context skeleton[:orderedItems] = ['!PLACEHOLDER!'] - skeleton = Oj.dump(skeleton) + skeleton = JSON.generate(skeleton) prepend, append = skeleton.split('"!PLACEHOLDER!"') add_comma = false @@ -44,7 +44,7 @@ class BackupService < BaseService end end - Oj.dump(item) + JSON.generate(item) end.join(',')) GC.start @@ -107,7 +107,7 @@ class BackupService < BaseService download_to_zip(zipfile, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists? download_to_zip(zipfile, account.header, "header#{File.extname(account.header.path)}") if account.header.exists? - json = Oj.dump(actor) + json = JSON.generate(actor) zipfile.get_output_stream('actor.json') do |io| io.write(json) @@ -118,7 +118,7 @@ class BackupService < BaseService skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'likes.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer) skeleton.delete(:totalItems) skeleton[:orderedItems] = ['!PLACEHOLDER!'] - skeleton = Oj.dump(skeleton) + skeleton = JSON.generate(skeleton) prepend, append = skeleton.split('"!PLACEHOLDER!"') zipfile.get_output_stream('likes.json') do |io| @@ -131,7 +131,7 @@ class BackupService < BaseService add_comma = true io.write(statuses.map do |status| - Oj.dump(ActivityPub::TagManager.instance.uri_for(status)) + JSON.generate(ActivityPub::TagManager.instance.uri_for(status)) end.join(',')) GC.start @@ -145,7 +145,7 @@ class BackupService < BaseService skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'bookmarks.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer) skeleton.delete(:totalItems) skeleton[:orderedItems] = ['!PLACEHOLDER!'] - skeleton = Oj.dump(skeleton) + skeleton = JSON.generate(skeleton) prepend, append = skeleton.split('"!PLACEHOLDER!"') zipfile.get_output_stream('bookmarks.json') do |io| @@ -157,7 +157,7 @@ class BackupService < BaseService add_comma = true io.write(statuses.map do |status| - Oj.dump(ActivityPub::TagManager.instance.uri_for(status)) + JSON.generate(ActivityPub::TagManager.instance.uri_for(status)) end.join(',')) GC.start diff --git a/spec/services/backup_service_spec.rb b/spec/services/backup_service_spec.rb index 878405a0fe..1dcebc24d2 100644 --- a/spec/services/backup_service_spec.rb +++ b/spec/services/backup_service_spec.rb @@ -56,7 +56,7 @@ RSpec.describe BackupService do def expect_outbox_export body = export_json_raw(:outbox) - json = Oj.load(body) + json = JSON.parse(body) aggregate_failures do expect(body.scan('@context').count).to eq 1 @@ -93,7 +93,7 @@ RSpec.describe BackupService do end def export_json(type) - Oj.load(export_json_raw(type)) + JSON.parse(export_json_raw(type)) end def include_create_item(status) From f460ad611ac27af1252b9260b7b8a9c370ee9d61 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 11:12:38 -0400 Subject: [PATCH 18/24] Use `to_json` call in web/push notification worker (#38233) --- app/workers/web/push_notification_worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb index a1f4e46690..0277aadfd8 100644 --- a/app/workers/web/push_notification_worker.rb +++ b/app/workers/web/push_notification_worker.rb @@ -101,7 +101,7 @@ class Web::PushNotificationWorker def push_notification_json I18n.with_locale(@subscription.locale.presence || I18n.default_locale) do - Oj.dump(serialized_notification.as_json) + serialized_notification.to_json end end From 8792d6f8408d93295930ea967632f4b404785382 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 11:16:43 -0400 Subject: [PATCH 19/24] Use `JSON.generate` in trivial string/hash conversion in specs (#38224) --- .../lib/activitypub/activity/announce_spec.rb | 4 +-- spec/lib/activitypub/activity/create_spec.rb | 6 ++-- .../activity/quote_request_spec.rb | 2 +- spec/lib/activitypub/activity_spec.rb | 2 +- spec/lib/activitypub/dereferencer_spec.rb | 2 +- spec/lib/activitypub/forwarder_spec.rb | 4 +-- spec/lib/webfinger_spec.rb | 8 ++--- spec/lib/webhooks/payload_renderer_spec.rb | 2 +- .../fetch_featured_collection_service_spec.rb | 20 ++++++------- ...h_featured_tags_collection_service_spec.rb | 8 ++--- .../fetch_remote_account_service_spec.rb | 26 ++++++++-------- .../fetch_remote_actor_service_spec.rb | 26 ++++++++-------- .../fetch_remote_key_service_spec.rb | 10 +++---- .../fetch_remote_status_service_spec.rb | 6 ++-- .../activitypub/fetch_replies_service_spec.rb | 6 ++-- .../process_collection_service_spec.rb | 2 +- .../process_status_update_service_spec.rb | 14 ++++----- .../synchronize_followers_service_spec.rb | 30 +++++++++---------- .../activitypub/verify_quote_service_spec.rb | 4 +-- .../fetch_remote_status_service_spec.rb | 2 +- spec/services/resolve_account_service_spec.rb | 10 +++---- .../software_update_check_service_spec.rb | 2 +- spec/support/streaming_client.rb | 2 +- .../fetch_all_replies_worker_spec.rb | 14 ++++----- .../activitypub/fetch_replies_worker_spec.rb | 2 +- 25 files changed, 107 insertions(+), 107 deletions(-) diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb index b556bfd6c2..4becf2320d 100644 --- a/spec/lib/activitypub/activity/announce_spec.rb +++ b/spec/lib/activitypub/activity/announce_spec.rb @@ -35,7 +35,7 @@ RSpec.describe ActivityPub::Activity::Announce do context 'when sender is followed by a local account' do before do Fabricate(:account).follow!(sender) - stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: JSON.generate(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' }) subject.perform end @@ -120,7 +120,7 @@ RSpec.describe ActivityPub::Activity::Announce do let(:object_json) { 'https://example.com/actor/hello-world' } before do - stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: JSON.generate(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' }) end context 'when the relay is enabled' do diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 19b6014af1..3e376db3dd 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -1029,7 +1029,7 @@ RSpec.describe ActivityPub::Activity::Create do end before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({ '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -1085,7 +1085,7 @@ RSpec.describe ActivityPub::Activity::Create do end before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({ '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -1217,7 +1217,7 @@ RSpec.describe ActivityPub::Activity::Create do before do stub_request(:get, object_json[:id]) .with(headers: { Authorization: "Bearer #{token}" }) - .to_return(body: Oj.dump(object_json), headers: { 'Content-Type': 'application/activity+json' }) + .to_return(body: JSON.generate(object_json), headers: { 'Content-Type': 'application/activity+json' }) subject.perform end diff --git a/spec/lib/activitypub/activity/quote_request_spec.rb b/spec/lib/activitypub/activity/quote_request_spec.rb index db80448a80..d68f01211d 100644 --- a/spec/lib/activitypub/activity/quote_request_spec.rb +++ b/spec/lib/activitypub/activity/quote_request_spec.rb @@ -86,7 +86,7 @@ RSpec.describe ActivityPub::Activity::QuoteRequest do context 'when trying to quote a quotable local status' do before do - stub_request(:get, 'https://example.com/unknown-status').to_return(status: 200, body: Oj.dump(status_json), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/unknown-status').to_return(status: 200, body: JSON.generate(status_json), headers: { 'Content-Type': 'application/activity+json' }) quoted_post.update(quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) end diff --git a/spec/lib/activitypub/activity_spec.rb b/spec/lib/activitypub/activity_spec.rb index d7d0700dc6..a4976eb867 100644 --- a/spec/lib/activitypub/activity_spec.rb +++ b/spec/lib/activitypub/activity_spec.rb @@ -89,7 +89,7 @@ RSpec.describe ActivityPub::Activity do before do sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender)) - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump(approval_payload)) + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate(approval_payload)) end context 'when getting them in order' do diff --git a/spec/lib/activitypub/dereferencer_spec.rb b/spec/lib/activitypub/dereferencer_spec.rb index 11078de866..13eedf518f 100644 --- a/spec/lib/activitypub/dereferencer_spec.rb +++ b/spec/lib/activitypub/dereferencer_spec.rb @@ -12,7 +12,7 @@ RSpec.describe ActivityPub::Dereferencer do let(:uri) { nil } before do - stub_request(:get, 'https://example.com/foo').to_return(body: Oj.dump(object), headers: { 'Content-Type' => 'application/activity+json' }) + stub_request(:get, 'https://example.com/foo').to_return(body: JSON.generate(object), headers: { 'Content-Type' => 'application/activity+json' }) end context 'with a URI' do diff --git a/spec/lib/activitypub/forwarder_spec.rb b/spec/lib/activitypub/forwarder_spec.rb index f72e334218..7276511a7d 100644 --- a/spec/lib/activitypub/forwarder_spec.rb +++ b/spec/lib/activitypub/forwarder_spec.rb @@ -54,8 +54,8 @@ RSpec.describe ActivityPub::Forwarder do it 'correctly forwards to expected remote followers' do expect { subject.forward! } - .to enqueue_sidekiq_job(ActivityPub::LowPriorityDeliveryWorker).with(Oj.dump(payload), anything, eve.preferred_inbox_url) - .and enqueue_sidekiq_job(ActivityPub::LowPriorityDeliveryWorker).with(Oj.dump(payload), anything, mallory.preferred_inbox_url) + .to enqueue_sidekiq_job(ActivityPub::LowPriorityDeliveryWorker).with(JSON.generate(payload), anything, eve.preferred_inbox_url) + .and enqueue_sidekiq_job(ActivityPub::LowPriorityDeliveryWorker).with(JSON.generate(payload), anything, mallory.preferred_inbox_url) end end end diff --git a/spec/lib/webfinger_spec.rb b/spec/lib/webfinger_spec.rb index e214a03536..7f2251b858 100644 --- a/spec/lib/webfinger_spec.rb +++ b/spec/lib/webfinger_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Webfinger do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } it 'correctly parses the response' do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) expect(subject.self_link_href).to eq 'https://example.com/alice' end @@ -20,7 +20,7 @@ RSpec.describe Webfinger do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }] } } it 'correctly parses the response' do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) expect(subject.self_link_href).to eq 'https://example.com/alice' end @@ -30,7 +30,7 @@ RSpec.describe Webfinger do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/json"' }] } } it 'raises an error' do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) expect { subject } .to raise_error(Webfinger::Error) @@ -53,7 +53,7 @@ RSpec.describe Webfinger do before do stub_request(:get, 'https://example.com/.well-known/host-meta').to_return(body: host_meta, headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://example.com/.well-known/nonStandardWebfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/.well-known/nonStandardWebfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'uses host meta details' do diff --git a/spec/lib/webhooks/payload_renderer_spec.rb b/spec/lib/webhooks/payload_renderer_spec.rb index 0623edd254..d4a330ea1c 100644 --- a/spec/lib/webhooks/payload_renderer_spec.rb +++ b/spec/lib/webhooks/payload_renderer_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Webhooks::PayloadRenderer do let(:event) { Webhooks::EventPresenter.new(type, object) } let(:payload) { ActiveModelSerializers::SerializableResource.new(event, serializer: REST::Admin::WebhookEventSerializer, scope: nil, scope_name: :current_user).as_json } - let(:json) { Oj.dump(payload) } + let(:json) { JSON.generate(payload) } describe '#render' do context 'when event is account.approved' do diff --git a/spec/services/activitypub/fetch_featured_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_collection_service_spec.rb index f0002bc388..24c5fa0a06 100644 --- a/spec/services/activitypub/fetch_featured_collection_service_spec.rb +++ b/spec/services/activitypub/fetch_featured_collection_service_spec.rb @@ -75,11 +75,11 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService do shared_examples 'sets pinned posts' do before do - stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: JSON.generate(status_json_pinned_known), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: JSON.generate(status_json_pinned_unknown_inlined), headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404) - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: Oj.dump(featured_with_null), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: JSON.generate(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: JSON.generate(featured_with_null), headers: { 'Content-Type': 'application/activity+json' }) subject end @@ -101,7 +101,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService do let(:collection_or_uri) { actor.featured_collection_url } before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets pinned posts' @@ -122,7 +122,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService do context 'when the endpoint is a Collection' do before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets pinned posts' @@ -139,7 +139,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService do end before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets pinned posts' @@ -148,7 +148,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService do let(:items) { 'https://example.com/account/pinned/unknown-reachable' } before do - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: JSON.generate(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) subject end @@ -175,7 +175,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService do end before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets pinned posts' @@ -184,7 +184,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService do let(:items) { 'https://example.com/account/pinned/unknown-reachable' } before do - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: JSON.generate(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) subject end diff --git a/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb index 59367b1e32..91b5267bdf 100644 --- a/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb +++ b/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb @@ -38,7 +38,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService do describe '#call' do context 'when the endpoint is a Collection' do before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_url).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets featured tags' @@ -46,7 +46,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService do context 'when the account already has featured tags' do before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_url).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) actor.featured_tags.create!(name: 'FoO') actor.featured_tags.create!(name: 'baz') @@ -67,7 +67,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService do end before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_url).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets featured tags' @@ -88,7 +88,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService do end before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_url).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'sets featured tags' diff --git a/spec/services/activitypub/fetch_remote_account_service_spec.rb b/spec/services/activitypub/fetch_remote_account_service_spec.rb index 7ebd3cdc70..30d693fa4b 100644 --- a/spec/services/activitypub/fetch_remote_account_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_account_service_spec.rb @@ -38,8 +38,8 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do before do actor[:inbox] = nil - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: JSON.generate(actor), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'fetches resource and looks up webfinger and returns nil' do @@ -54,8 +54,8 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: JSON.generate(actor), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'fetches resource and looks up webfinger and sets attributes' do @@ -75,9 +75,9 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: JSON.generate(actor), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'fetches resource and looks up webfinger and follows redirection and sets attributes' do @@ -98,8 +98,8 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: JSON.generate(actor), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'fetches resource and looks up webfinger and does not create account' do @@ -114,9 +114,9 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: JSON.generate(actor), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'fetches resource and looks up webfinger and follows redirect and does not create account' do @@ -130,7 +130,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do context 'with wrong id' do it 'does not create account' do - expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil + expect(subject.call('https://fake.address/@foo', prefetched_body: JSON.generate(actor))).to be_nil end end end diff --git a/spec/services/activitypub/fetch_remote_actor_service_spec.rb b/spec/services/activitypub/fetch_remote_actor_service_spec.rb index 975e0799dd..36457c207c 100644 --- a/spec/services/activitypub/fetch_remote_actor_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_actor_service_spec.rb @@ -38,8 +38,8 @@ RSpec.describe ActivityPub::FetchRemoteActorService do before do actor[:inbox] = nil - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: JSON.generate(actor), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'fetches resource and looks up webfinger and returns nil' do @@ -54,8 +54,8 @@ RSpec.describe ActivityPub::FetchRemoteActorService do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: JSON.generate(actor), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'fetches resource and looks up webfinger and sets values' do @@ -75,9 +75,9 @@ RSpec.describe ActivityPub::FetchRemoteActorService do let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: JSON.generate(actor), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'fetches resource and looks up webfinger and follows redirect and sets values' do @@ -98,8 +98,8 @@ RSpec.describe ActivityPub::FetchRemoteActorService do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: JSON.generate(actor), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'fetches resource and looks up webfinger and does not create account' do @@ -114,9 +114,9 @@ RSpec.describe ActivityPub::FetchRemoteActorService do let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: JSON.generate(actor), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'fetches resource and looks up webfinger and follows redirect and does not create account' do @@ -130,7 +130,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService do context 'with wrong id' do it 'does not create account' do - expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil + expect(subject.call('https://fake.address/@foo', prefetched_body: JSON.generate(actor))).to be_nil end end end diff --git a/spec/services/activitypub/fetch_remote_key_service_spec.rb b/spec/services/activitypub/fetch_remote_key_service_spec.rb index ddd1a8067e..635a07c26b 100644 --- a/spec/services/activitypub/fetch_remote_key_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_key_service_spec.rb @@ -50,8 +50,8 @@ RSpec.describe ActivityPub::FetchRemoteKeyService do end before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/alice').to_return(body: JSON.generate(actor), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end describe '#call' do @@ -59,7 +59,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService do context 'when the key is a sub-object from the actor' do before do - stub_request(:get, public_key_id).to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, public_key_id).to_return(body: JSON.generate(actor), headers: { 'Content-Type': 'application/activity+json' }) end it 'returns the expected account' do @@ -71,7 +71,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService do let(:public_key_id) { 'https://example.com/alice-public-key.json' } before do - stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, public_key_id).to_return(body: JSON.generate(key_json.merge({ '@context': ['https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' }) end it 'returns the expected account' do @@ -84,7 +84,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService do let(:actor_public_key) { 'https://example.com/alice-public-key.json' } before do - stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, public_key_id).to_return(body: JSON.generate(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' }) end it 'returns the nil' do diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 6afee5f25e..e37fcca7a0 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do let(:follower) { Fabricate(:account, username: 'alice') } let(:follow) { nil } - let(:response) { { body: Oj.dump(object), headers: { 'content-type': 'application/activity+json' } } } + let(:response) { { body: JSON.generate(object), headers: { 'content-type': 'application/activity+json' } } } let(:existing_status) { nil } let(:note) do @@ -369,7 +369,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do end it 'creates statuses but not more than limit allows' do - expect { subject.call(object[:id], prefetched_body: Oj.dump(object)) } + expect { subject.call(object[:id], prefetched_body: JSON.generate(object)) } .to change { sender.statuses.count }.by_at_least(2) .and change { sender.statuses.count }.by_at_most(3) end @@ -419,7 +419,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do end it 'creates statuses but not more than limit allows' do - expect { subject.call(object[:id], prefetched_body: Oj.dump(object)) } + expect { subject.call(object[:id], prefetched_body: JSON.generate(object)) } .to change { sender.statuses.count }.by_at_least(2) .and change { sender.statuses.count }.by_at_most(3) end diff --git a/spec/services/activitypub/fetch_replies_service_spec.rb b/spec/services/activitypub/fetch_replies_service_spec.rb index 36159309f1..1442755c53 100644 --- a/spec/services/activitypub/fetch_replies_service_spec.rb +++ b/spec/services/activitypub/fetch_replies_service_spec.rb @@ -58,7 +58,7 @@ RSpec.describe ActivityPub::FetchRepliesService do context 'when passing the URL to the collection' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it 'spawns workers for up to 5 replies on the same server' do @@ -93,7 +93,7 @@ RSpec.describe ActivityPub::FetchRepliesService do context 'when passing the URL to the collection' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it 'spawns workers for up to 5 replies on the same server' do @@ -132,7 +132,7 @@ RSpec.describe ActivityPub::FetchRepliesService do context 'when passing the URL to the collection' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it 'spawns workers for up to 5 replies on the same server' do diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb index 74df0f9106..cee4272c22 100644 --- a/spec/services/activitypub/process_collection_service_spec.rb +++ b/spec/services/activitypub/process_collection_service_spec.rb @@ -21,7 +21,7 @@ RSpec.describe ActivityPub::ProcessCollectionService do } end - let(:json) { Oj.dump(payload) } + let(:json) { JSON.generate(payload) } describe '#call' do context 'when actor is suspended' do diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index aca2feb008..8279411a33 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -23,7 +23,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do ], } end - let(:json) { Oj.load(Oj.dump(payload)) } + let(:json) { Oj.load(JSON.generate(payload)) } let(:alice) { Fabricate(:account) } let(:bob) { Fabricate(:account) } @@ -545,7 +545,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do end before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({ '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -610,7 +610,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do end before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({ '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -819,7 +819,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do end before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({ '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -884,7 +884,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do end before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({ '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -1127,7 +1127,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do end before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({ '@context': [ 'https://www.w3.org/ns/activitystreams', { @@ -1235,7 +1235,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do end before do - stub_request(:get, second_approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + stub_request(:get, second_approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({ '@context': [ 'https://www.w3.org/ns/activitystreams', { diff --git a/spec/services/activitypub/synchronize_followers_service_spec.rb b/spec/services/activitypub/synchronize_followers_service_spec.rb index 813658d149..c41783f7c4 100644 --- a/spec/services/activitypub/synchronize_followers_service_spec.rb +++ b/spec/services/activitypub/synchronize_followers_service_spec.rb @@ -55,7 +55,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do context 'when the endpoint is a Collection of actor URIs' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -72,7 +72,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -93,7 +93,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -102,7 +102,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do context 'when the endpoint is a paginated Collection of actor URIs split across multiple pages' do before do stub_request(:get, 'https://example.com/partial-followers') - .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({ '@context': 'https://www.w3.org/ns/activitystreams', type: 'Collection', id: 'https://example.com/partial-followers', @@ -110,7 +110,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do })) stub_request(:get, 'https://example.com/partial-followers/1') - .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({ '@context': 'https://www.w3.org/ns/activitystreams', type: 'CollectionPage', id: 'https://example.com/partial-followers/1', @@ -120,7 +120,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do })) stub_request(:get, 'https://example.com/partial-followers/2') - .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({ '@context': 'https://www.w3.org/ns/activitystreams', type: 'CollectionPage', id: 'https://example.com/partial-followers/2', @@ -135,7 +135,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do context 'when the endpoint is a paginated Collection of actor URIs split across, but one page errors out' do before do stub_request(:get, 'https://example.com/partial-followers') - .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({ '@context': 'https://www.w3.org/ns/activitystreams', type: 'Collection', id: 'https://example.com/partial-followers', @@ -143,7 +143,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do })) stub_request(:get, 'https://example.com/partial-followers/1') - .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({ '@context': 'https://www.w3.org/ns/activitystreams', type: 'CollectionPage', id: 'https://example.com/partial-followers/1', @@ -185,7 +185,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do before do stub_const('ActivityPub::SynchronizeFollowersService::MAX_COLLECTION_PAGES', 1) - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it 'confirms pending follow request but does not remove extra followers' do @@ -213,7 +213,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do context 'when the endpoint is a Collection of actor URIs' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -230,7 +230,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -251,7 +251,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'synchronizes followers' @@ -263,7 +263,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do context 'when the endpoint is a Collection of actor URIs' do before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it 'does not remove followers' do @@ -286,7 +286,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it 'does not remove followers' do @@ -313,7 +313,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do end before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, collection_uri).to_return(status: 200, body: JSON.generate(payload), headers: { 'Content-Type': 'application/activity+json' }) end it 'does not remove followers' do diff --git a/spec/services/activitypub/verify_quote_service_spec.rb b/spec/services/activitypub/verify_quote_service_spec.rb index 94b9e33ed3..5c9248dbb9 100644 --- a/spec/services/activitypub/verify_quote_service_spec.rb +++ b/spec/services/activitypub/verify_quote_service_spec.rb @@ -76,7 +76,7 @@ RSpec.describe ActivityPub::VerifyQuoteService do before do stub_request(:get, approval_uri) - .to_return(status: 200, body: Oj.dump(json), headers: { 'Content-Type': 'application/activity+json' }) + .to_return(status: 200, body: JSON.generate(json), headers: { 'Content-Type': 'application/activity+json' }) end context 'with a valid activity for already-fetched posts' do @@ -179,7 +179,7 @@ RSpec.describe ActivityPub::VerifyQuoteService do context 'with a valid activity for already-fetched posts, with a pre-fetched approval' do it 'updates the status without fetching the activity' do - expect { subject.call(quote, prefetched_approval: Oj.dump(json)) } + expect { subject.call(quote, prefetched_approval: JSON.generate(json)) } .to change(quote, :state).to('accepted') expect(a_request(:get, approval_uri)) diff --git a/spec/services/fetch_remote_status_service_spec.rb b/spec/services/fetch_remote_status_service_spec.rb index a9c61e7b4e..2b7ff9cbdc 100644 --- a/spec/services/fetch_remote_status_service_spec.rb +++ b/spec/services/fetch_remote_status_service_spec.rb @@ -19,7 +19,7 @@ RSpec.describe FetchRemoteStatusService do context 'when protocol is :activitypub' do subject { described_class.new.call(note[:id], prefetched_body: prefetched_body) } - let(:prefetched_body) { Oj.dump(note) } + let(:prefetched_body) { JSON.generate(note) } before do subject diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index 1bd4e9a8e3..897f7cf44f 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -103,7 +103,7 @@ RSpec.describe ResolveAccountService do context 'with a legitimate webfinger redirection' do before do webfinger = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'returns new remote account' do @@ -121,7 +121,7 @@ RSpec.describe ResolveAccountService do context 'with a misconfigured redirection' do before do webfinger = { subject: 'acct:Foo@redirected.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) end it 'returns new remote account' do @@ -139,9 +139,9 @@ RSpec.describe ResolveAccountService do context 'with too many webfinger redirections' do before do webfinger = { subject: 'acct:foo@evil.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: JSON.generate(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) webfinger2 = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://evil.example.com/.well-known/webfinger?resource=acct:foo@evil.example.com').to_return(body: Oj.dump(webfinger2), headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://evil.example.com/.well-known/webfinger?resource=acct:foo@evil.example.com').to_return(body: JSON.generate(webfinger2), headers: { 'Content-Type': 'application/jrd+json' }) end it 'does not return a new remote account' do @@ -150,7 +150,7 @@ RSpec.describe ResolveAccountService do end context 'with webfinger response subject missing a host value' do - let(:body) { Oj.dump({ subject: 'user@' }) } + let(:body) { JSON.generate({ subject: 'user@' }) } let(:url) { 'https://host.example/.well-known/webfinger?resource=acct:user@host.example' } before do diff --git a/spec/services/software_update_check_service_spec.rb b/spec/services/software_update_check_service_spec.rb index 73ffe6b899..ac342dce94 100644 --- a/spec/services/software_update_check_service_spec.rb +++ b/spec/services/software_update_check_service_spec.rb @@ -82,7 +82,7 @@ RSpec.describe SoftwareUpdateCheckService do end before do - stub_request(:get, full_update_check_url).to_return(body: Oj.dump(server_json)) + stub_request(:get, full_update_check_url).to_return(body: JSON.generate(server_json)) end it 'updates the list of known updates' do diff --git a/spec/support/streaming_client.rb b/spec/support/streaming_client.rb index 02186e781c..c9a97ab060 100644 --- a/spec/support/streaming_client.rb +++ b/spec/support/streaming_client.rb @@ -152,7 +152,7 @@ class StreamingClient end def subscribe(channel, **params) - send(Oj.dump({ type: 'subscribe', stream: channel }.merge(params))) + send(JSON.generate({ type: 'subscribe', stream: channel }.merge(params))) end def wait_for(event = nil) diff --git a/spec/workers/activitypub/fetch_all_replies_worker_spec.rb b/spec/workers/activitypub/fetch_all_replies_worker_spec.rb index 9795c4619a..c7b107ac81 100644 --- a/spec/workers/activitypub/fetch_all_replies_worker_spec.rb +++ b/spec/workers/activitypub/fetch_all_replies_worker_spec.rb @@ -126,11 +126,11 @@ RSpec.describe ActivityPub::FetchAllRepliesWorker do all_items.each do |item| next if [top_note_uri, reply_note_uri].include? item - stub_request(:get, item).to_return(status: 200, body: Oj.dump(empty_object), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, item).to_return(status: 200, body: JSON.generate(empty_object), headers: { 'Content-Type': 'application/activity+json' }) end - stub_request(:get, top_note_uri).to_return(status: 200, body: Oj.dump(top_object), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, reply_note_uri).to_return(status: 200, body: Oj.dump(reply_object), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, top_note_uri).to_return(status: 200, body: JSON.generate(top_object), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, reply_note_uri).to_return(status: 200, body: JSON.generate(reply_object), headers: { 'Content-Type': 'application/activity+json' }) end shared_examples 'fetches all replies' do @@ -180,8 +180,8 @@ RSpec.describe ActivityPub::FetchAllRepliesWorker do end before do - stub_request(:get, top_collection_uri).to_return(status: 200, body: Oj.dump(replies_top), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, reply_collection_uri).to_return(status: 200, body: Oj.dump(replies_nested), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, top_collection_uri).to_return(status: 200, body: JSON.generate(replies_top), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, reply_collection_uri).to_return(status: 200, body: JSON.generate(replies_nested), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'fetches all replies' @@ -254,8 +254,8 @@ RSpec.describe ActivityPub::FetchAllRepliesWorker do end before do - stub_request(:get, top_page_2_uri).to_return(status: 200, body: Oj.dump(top_page_two), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, reply_page_2_uri).to_return(status: 200, body: Oj.dump(reply_page_two), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, top_page_2_uri).to_return(status: 200, body: JSON.generate(top_page_two), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, reply_page_2_uri).to_return(status: 200, body: JSON.generate(reply_page_two), headers: { 'Content-Type': 'application/activity+json' }) end it_behaves_like 'fetches all replies' diff --git a/spec/workers/activitypub/fetch_replies_worker_spec.rb b/spec/workers/activitypub/fetch_replies_worker_spec.rb index 56d19705a4..5eea3fcbcf 100644 --- a/spec/workers/activitypub/fetch_replies_worker_spec.rb +++ b/spec/workers/activitypub/fetch_replies_worker_spec.rb @@ -17,7 +17,7 @@ RSpec.describe ActivityPub::FetchRepliesWorker do } end - let(:json) { Oj.dump(payload) } + let(:json) { JSON.generate(payload) } describe 'perform' do it 'performs a request if the collection URI is from the same host' do From 703f2d0263f8ce858eaa924cbc5b00b19ee1b2e3 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 16 Mar 2026 11:17:57 -0400 Subject: [PATCH 20/24] Use implicit `to_json` call in app/services (#38225) --- app/services/activitypub/synchronize_followers_service.rb | 2 +- app/services/after_block_domain_from_account_service.rb | 2 +- app/services/authorize_follow_service.rb | 2 +- app/services/block_service.rb | 2 +- app/services/create_featured_tag_service.rb | 2 +- app/services/delete_account_service.rb | 6 +++--- app/services/favourite_service.rb | 2 +- app/services/follow_service.rb | 2 +- app/services/reject_follow_service.rb | 2 +- app/services/remove_domains_from_followers_service.rb | 2 +- app/services/remove_featured_tag_service.rb | 2 +- app/services/remove_from_followers_service.rb | 2 +- app/services/remove_status_service.rb | 2 +- app/services/report_service.rb | 2 +- app/services/revoke_collection_item_service.rb | 2 +- app/services/revoke_quote_service.rb | 2 +- app/services/suspend_account_service.rb | 4 ++-- app/services/unblock_service.rb | 2 +- app/services/unfavourite_service.rb | 2 +- app/services/unfollow_service.rb | 4 ++-- app/services/unsuspend_account_service.rb | 2 +- app/services/vote_service.rb | 2 +- 22 files changed, 26 insertions(+), 26 deletions(-) diff --git a/app/services/activitypub/synchronize_followers_service.rb b/app/services/activitypub/synchronize_followers_service.rb index 9e0b452929..30f475ed07 100644 --- a/app/services/activitypub/synchronize_followers_service.rb +++ b/app/services/activitypub/synchronize_followers_service.rb @@ -62,7 +62,7 @@ class ActivityPub::SynchronizeFollowersService < BaseService end def build_undo_follow_json(follow) - Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)) + serialize_payload(follow, ActivityPub::UndoFollowSerializer).to_json end # Only returns true if the whole collection has been processed diff --git a/app/services/after_block_domain_from_account_service.rb b/app/services/after_block_domain_from_account_service.rb index fc5dc65681..549d508674 100644 --- a/app/services/after_block_domain_from_account_service.rb +++ b/app/services/after_block_domain_from_account_service.rb @@ -54,7 +54,7 @@ class AfterBlockDomainFromAccountService < BaseService return unless follow.account.activitypub? - ActivityPub::DeliveryWorker.perform_async(Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), @account.id, follow.account.inbox_url) + ActivityPub::DeliveryWorker.perform_async(serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json, @account.id, follow.account.inbox_url) end def notify_of_severed_relationships! diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb index 49bef727e6..1c2c6da3ce 100644 --- a/app/services/authorize_follow_service.rb +++ b/app/services/authorize_follow_service.rb @@ -22,6 +22,6 @@ class AuthorizeFollowService < BaseService end def build_json(follow_request) - Oj.dump(serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer)) + serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer).to_json end end diff --git a/app/services/block_service.rb b/app/services/block_service.rb index 98229d98c0..cbd0e8e75b 100644 --- a/app/services/block_service.rb +++ b/app/services/block_service.rb @@ -26,6 +26,6 @@ class BlockService < BaseService end def build_json(block) - Oj.dump(serialize_payload(block, ActivityPub::BlockSerializer)) + serialize_payload(block, ActivityPub::BlockSerializer).to_json end end diff --git a/app/services/create_featured_tag_service.rb b/app/services/create_featured_tag_service.rb index 298bdb5928..b8cb5903aa 100644 --- a/app/services/create_featured_tag_service.rb +++ b/app/services/create_featured_tag_service.rb @@ -26,6 +26,6 @@ class CreateFeaturedTagService < BaseService private def build_json(featured_tag) - Oj.dump(serialize_payload(featured_tag, ActivityPub::AddHashtagSerializer, signer: @account)) + serialize_payload(featured_tag, ActivityPub::AddHashtagSerializer, signer: @account).to_json end end diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb index 6557dda48f..fa19d6ee50 100644 --- a/app/services/delete_account_service.rb +++ b/app/services/delete_account_service.rb @@ -114,7 +114,7 @@ class DeleteAccountService < BaseService # we have to force it to unfollow them. ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow| - [Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url] + [serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json, follow.target_account_id, @account.inbox_url] end end @@ -126,7 +126,7 @@ class DeleteAccountService < BaseService # if the remote account gets un-suspended. ActivityPub::DeliveryWorker.push_bulk(Follow.where(target_account: @account)) do |follow| - [Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)), follow.account_id, @account.inbox_url] + [serialize_payload(follow, ActivityPub::UndoFollowSerializer).to_json, follow.account_id, @account.inbox_url] end end @@ -285,7 +285,7 @@ class DeleteAccountService < BaseService end def delete_actor_json - @delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account, always_sign: true)) + @delete_actor_json ||= serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account, always_sign: true).to_json end def delivery_inboxes diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index ded50187f7..7638d7f257 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -42,6 +42,6 @@ class FavouriteService < BaseService end def build_json(favourite) - Oj.dump(serialize_payload(favourite, ActivityPub::LikeSerializer)) + serialize_payload(favourite, ActivityPub::LikeSerializer).to_json end end diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 5ff1b63503..c2d2956a98 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -90,7 +90,7 @@ class FollowService < BaseService end def build_json(follow_request) - Oj.dump(serialize_payload(follow_request, ActivityPub::FollowSerializer)) + serialize_payload(follow_request, ActivityPub::FollowSerializer).to_json end def follow_options diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb index bc0000c8c8..4ced57bdf2 100644 --- a/app/services/reject_follow_service.rb +++ b/app/services/reject_follow_service.rb @@ -17,6 +17,6 @@ class RejectFollowService < BaseService end def build_json(follow_request) - Oj.dump(serialize_payload(follow_request, ActivityPub::RejectFollowSerializer)) + serialize_payload(follow_request, ActivityPub::RejectFollowSerializer).to_json end end diff --git a/app/services/remove_domains_from_followers_service.rb b/app/services/remove_domains_from_followers_service.rb index d76763409d..ed01b26e16 100644 --- a/app/services/remove_domains_from_followers_service.rb +++ b/app/services/remove_domains_from_followers_service.rb @@ -18,6 +18,6 @@ class RemoveDomainsFromFollowersService < BaseService end def build_json(follow) - Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) + serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json end end diff --git a/app/services/remove_featured_tag_service.rb b/app/services/remove_featured_tag_service.rb index 4fdd43eb6a..69e8a47bf4 100644 --- a/app/services/remove_featured_tag_service.rb +++ b/app/services/remove_featured_tag_service.rb @@ -26,6 +26,6 @@ class RemoveFeaturedTagService < BaseService private def build_json(featured_tag) - Oj.dump(serialize_payload(featured_tag, ActivityPub::RemoveHashtagSerializer, signer: @account)) + serialize_payload(featured_tag, ActivityPub::RemoveHashtagSerializer, signer: @account).to_json end end diff --git a/app/services/remove_from_followers_service.rb b/app/services/remove_from_followers_service.rb index 007d5b1fdd..22fb72cd64 100644 --- a/app/services/remove_from_followers_service.rb +++ b/app/services/remove_from_followers_service.rb @@ -18,6 +18,6 @@ class RemoveFromFollowersService < BaseService end def build_json(follow) - Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) + serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 042a20ec79..bb86f346ac 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -103,7 +103,7 @@ class RemoveStatusService < BaseService end def signed_activity_json - @signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteNoteSerializer, signer: @account, always_sign: true)) + @signed_activity_json ||= serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteNoteSerializer, signer: @account, always_sign: true).to_json end def remove_reblogs diff --git a/app/services/report_service.rb b/app/services/report_service.rb index a666450af0..3e418dc85a 100644 --- a/app/services/report_service.rb +++ b/app/services/report_service.rb @@ -98,7 +98,7 @@ class ReportService < BaseService end def payload - Oj.dump(serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account)) + serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account).to_json end def some_local_account diff --git a/app/services/revoke_collection_item_service.rb b/app/services/revoke_collection_item_service.rb index d299b567f2..c0dc70e952 100644 --- a/app/services/revoke_collection_item_service.rb +++ b/app/services/revoke_collection_item_service.rb @@ -19,6 +19,6 @@ class RevokeCollectionItemService < BaseService end def signed_activity_json - @signed_activity_json ||= Oj.dump(serialize_payload(@collection_item, ActivityPub::DeleteFeatureAuthorizationSerializer, signer: @account, always_sign: true)) + @signed_activity_json ||= serialize_payload(@collection_item, ActivityPub::DeleteFeatureAuthorizationSerializer, signer: @account, always_sign: true).to_json end end diff --git a/app/services/revoke_quote_service.rb b/app/services/revoke_quote_service.rb index 346fba8970..1bc69c1f51 100644 --- a/app/services/revoke_quote_service.rb +++ b/app/services/revoke_quote_service.rb @@ -39,6 +39,6 @@ class RevokeQuoteService < BaseService end def signed_activity_json - @signed_activity_json ||= Oj.dump(serialize_payload(@quote, ActivityPub::DeleteQuoteAuthorizationSerializer, signer: @account, always_sign: true, force_approval_id: true)) + @signed_activity_json ||= serialize_payload(@quote, ActivityPub::DeleteQuoteAuthorizationSerializer, signer: @account, always_sign: true, force_approval_id: true).to_json end end diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 666b64cacf..1ec868a961 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -34,7 +34,7 @@ class SuspendAccountService < BaseService Follow.where(account: @account).find_in_batches do |follows| ActivityPub::DeliveryWorker.push_bulk(follows) do |follow| - [Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url] + [serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json, follow.target_account_id, @account.inbox_url] end follows.each(&:destroy) @@ -72,6 +72,6 @@ class SuspendAccountService < BaseService end def signed_activity_json - @signed_activity_json ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account)) + @signed_activity_json ||= serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account).to_json end end diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb index c263ac8afe..31067618a9 100644 --- a/app/services/unblock_service.rb +++ b/app/services/unblock_service.rb @@ -18,6 +18,6 @@ class UnblockService < BaseService end def build_json(unblock) - Oj.dump(serialize_payload(unblock, ActivityPub::UndoBlockSerializer)) + serialize_payload(unblock, ActivityPub::UndoBlockSerializer).to_json end end diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb index 37917a64f1..2f422c4251 100644 --- a/app/services/unfavourite_service.rb +++ b/app/services/unfavourite_service.rb @@ -18,6 +18,6 @@ class UnfavouriteService < BaseService end def build_json(favourite) - Oj.dump(serialize_payload(favourite, ActivityPub::UndoLikeSerializer)) + serialize_payload(favourite, ActivityPub::UndoLikeSerializer).to_json end end diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index 385aa0c7b1..a77f8c012d 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -68,10 +68,10 @@ class UnfollowService < BaseService end def build_json(follow) - Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)) + serialize_payload(follow, ActivityPub::UndoFollowSerializer).to_json end def build_reject_json(follow) - Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) + serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json end end diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb index 1a52e80d24..95cb18606f 100644 --- a/app/services/unsuspend_account_service.rb +++ b/app/services/unsuspend_account_service.rb @@ -63,6 +63,6 @@ class UnsuspendAccountService < BaseService end def signed_activity_json - @signed_activity_json ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account)) + @signed_activity_json ||= serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account).to_json end end diff --git a/app/services/vote_service.rb b/app/services/vote_service.rb index 878350388b..f30748faed 100644 --- a/app/services/vote_service.rb +++ b/app/services/vote_service.rb @@ -65,7 +65,7 @@ class VoteService < BaseService end def build_json(vote) - Oj.dump(serialize_payload(vote, ActivityPub::VoteSerializer)) + serialize_payload(vote, ActivityPub::VoteSerializer).to_json end def increment_voters_count! From 4328807f28f28eed7afbd650aef45f5bcd436d38 Mon Sep 17 00:00:00 2001 From: Echo Date: Mon, 16 Mar 2026 16:56:30 +0100 Subject: [PATCH 21/24] Profile editing: Finish image editing (#38235) --- app/javascript/mastodon/api/accounts.ts | 6 + app/javascript/mastodon/api_types/profile.ts | 2 + .../components/details/details.stories.tsx | 29 ++++ .../mastodon/components/details/index.tsx | 35 +++++ .../components/details/styles.module.scss | 25 +++ .../mastodon/features/account_edit/index.tsx | 7 +- .../account_edit/modals/image_alt.tsx | 143 +++++++++++++++++- .../account_edit/modals/image_delete.tsx | 40 ++++- .../account_edit/modals/image_upload.tsx | 61 +------- .../features/account_edit/modals/index.ts | 2 +- .../account_edit/modals/styles.module.scss | 39 ++--- .../account_edit/modals/verified_modal.tsx | 13 +- .../features/account_edit/styles.module.scss | 13 ++ .../components/account_header.tsx | 1 + app/javascript/mastodon/locales/en.json | 13 +- .../mastodon/reducers/slices/profile_edit.ts | 48 +++++- 16 files changed, 373 insertions(+), 104 deletions(-) create mode 100644 app/javascript/mastodon/components/details/details.stories.tsx create mode 100644 app/javascript/mastodon/components/details/index.tsx create mode 100644 app/javascript/mastodon/components/details/styles.module.scss diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index 15156e156c..fc6e38fbc8 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -69,3 +69,9 @@ export const apiGetProfile = () => apiRequestGet('v1/profile'); export const apiPatchProfile = (params: ApiProfileUpdateParams | FormData) => apiRequestPatch('v1/profile', params); + +export const apiDeleteProfileAvatar = () => + apiRequestDelete('v1/profile/avatar'); + +export const apiDeleteProfileHeader = () => + apiRequestDelete('v1/profile/header'); diff --git a/app/javascript/mastodon/api_types/profile.ts b/app/javascript/mastodon/api_types/profile.ts index 9814bddde9..acc3b46787 100644 --- a/app/javascript/mastodon/api_types/profile.ts +++ b/app/javascript/mastodon/api_types/profile.ts @@ -27,6 +27,8 @@ export interface ApiProfileJSON { export type ApiProfileUpdateParams = Partial< Pick< ApiProfileJSON, + | 'avatar_description' + | 'header_description' | 'display_name' | 'note' | 'locked' diff --git a/app/javascript/mastodon/components/details/details.stories.tsx b/app/javascript/mastodon/components/details/details.stories.tsx new file mode 100644 index 0000000000..3bc833f313 --- /dev/null +++ b/app/javascript/mastodon/components/details/details.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Details } from './index'; + +const meta = { + component: Details, + title: 'Components/Details', + args: { + summary: 'Here is the summary title', + children: ( +

+ And here are the details that are hidden until you click the summary. +

+ ), + }, + render(props) { + return ( +
+
+
+ ); + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Plain: Story = {}; diff --git a/app/javascript/mastodon/components/details/index.tsx b/app/javascript/mastodon/components/details/index.tsx new file mode 100644 index 0000000000..aac92e8f77 --- /dev/null +++ b/app/javascript/mastodon/components/details/index.tsx @@ -0,0 +1,35 @@ +import { forwardRef } from 'react'; +import type { ComponentPropsWithoutRef, ReactNode } from 'react'; + +import classNames from 'classnames'; + +import ExpandArrowIcon from '@/material-icons/400-24px/expand_more.svg?react'; + +import { Icon } from '../icon'; + +import classes from './styles.module.scss'; + +export const Details = forwardRef< + HTMLDetailsElement, + { + summary: ReactNode; + children: ReactNode; + className?: string; + } & ComponentPropsWithoutRef<'details'> +>(({ summary, children, className, ...rest }, ref) => { + return ( +
+ + {summary} + + + + {children} +
+ ); +}); +Details.displayName = 'Details'; diff --git a/app/javascript/mastodon/components/details/styles.module.scss b/app/javascript/mastodon/components/details/styles.module.scss new file mode 100644 index 0000000000..03aace8a62 --- /dev/null +++ b/app/javascript/mastodon/components/details/styles.module.scss @@ -0,0 +1,25 @@ +.details { + color: var(--color-text-secondary); + font-size: 13px; + margin-top: 8px; + + summary { + cursor: pointer; + font-weight: 600; + list-style: none; + margin-bottom: 8px; + text-decoration: underline; + text-decoration-style: dotted; + } + + :global(.icon) { + width: 1.4em; + height: 1.4em; + vertical-align: middle; + transition: transform 0.2s ease-in-out; + } + + &[open] :global(.icon) { + transform: rotate(-180deg); + } +} diff --git a/app/javascript/mastodon/features/account_edit/index.tsx b/app/javascript/mastodon/features/account_edit/index.tsx index a621de4c5d..0157681753 100644 --- a/app/javascript/mastodon/features/account_edit/index.tsx +++ b/app/javascript/mastodon/features/account_edit/index.tsx @@ -7,6 +7,7 @@ import { useHistory } from 'react-router-dom'; import type { ModalType } from '@/mastodon/actions/modal'; import { openModal } from '@/mastodon/actions/modal'; +import { AccountBio } from '@/mastodon/components/account_bio'; import { Avatar } from '@/mastodon/components/avatar'; import { Button } from '@/mastodon/components/button'; import { DismissibleCallout } from '@/mastodon/components/callout/dismissible'; @@ -201,7 +202,11 @@ export const AccountEdit: FC = () => { /> } > - + = ({ onClose }) => { - return ; +> = ({ onClose, location }) => { + const { profile, isPending } = useAppSelector((state) => state.profileEdit); + + const initialAlt = profile?.[`${location}Description`]; + const imageSrc = profile?.[`${location}Static`]; + + const [altText, setAltText] = useState(initialAlt ?? ''); + + const dispatch = useAppDispatch(); + const handleSave = useCallback(() => { + void dispatch( + patchProfile({ + [`${location}_description`]: altText, + }), + ).then(onClose); + }, [altText, dispatch, location, onClose]); + + if (!imageSrc) { + return ; + } + + return ( + + ) : ( + + ) + } + onClose={onClose} + onConfirm={handleSave} + confirm={ + + } + updating={isPending} + > +
+ +
+
+ ); +}; + +export const ImageAltTextField: FC<{ + imageSrc: string; + altText: string; + onChange: (altText: string) => void; +}> = ({ imageSrc, altText, onChange }) => { + const altLimit = useAppSelector( + (state) => + state.server.getIn( + ['server', 'configuration', 'media_attachments', 'description_limit'], + 150, + ) as number, + ); + + const handleChange: ChangeEventHandler = useCallback( + (event) => { + onChange(event.currentTarget.value); + }, + [onChange], + ); + + return ( + <> + + +
+ + } + hint={ + + } + onChange={handleChange} + value={altText} + /> + +
+ +
+ } + className={classes.altHint} + > +
    {chunks}
, + li: (chunks) =>
  • {chunks}
  • , + }} + tagName='div' + /> +
    + + ); }; diff --git a/app/javascript/mastodon/features/account_edit/modals/image_delete.tsx b/app/javascript/mastodon/features/account_edit/modals/image_delete.tsx index 211dd35bed..50bcf3d8a1 100644 --- a/app/javascript/mastodon/features/account_edit/modals/image_delete.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/image_delete.tsx @@ -1,12 +1,48 @@ +import { useCallback } from 'react'; import type { FC } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { Button } from '@/mastodon/components/button'; +import { deleteImage } from '@/mastodon/reducers/slices/profile_edit'; import type { ImageLocation } from '@/mastodon/reducers/slices/profile_edit'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import { DialogModal } from '../../ui/components/dialog_modal'; import type { DialogModalProps } from '../../ui/components/dialog_modal'; export const ImageDeleteModal: FC< DialogModalProps & { location: ImageLocation } -> = ({ onClose }) => { - return ; +> = ({ onClose, location }) => { + const isPending = useAppSelector((state) => state.profileEdit.isPending); + const dispatch = useAppDispatch(); + const handleDelete = useCallback(() => { + void dispatch(deleteImage({ location })).then(onClose); + }, [dispatch, location, onClose]); + + return ( + + } + buttons={ + + } + > + + + ); }; diff --git a/app/javascript/mastodon/features/account_edit/modals/image_upload.tsx b/app/javascript/mastodon/features/account_edit/modals/image_upload.tsx index bf6473a9a0..ccf65cceed 100644 --- a/app/javascript/mastodon/features/account_edit/modals/image_upload.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/image_upload.tsx @@ -8,9 +8,6 @@ import Cropper from 'react-easy-crop'; import { setDragUploadEnabled } from '@/mastodon/actions/compose_typed'; import { Button } from '@/mastodon/components/button'; -import { Callout } from '@/mastodon/components/callout'; -import { CharacterCounter } from '@/mastodon/components/character_counter'; -import { TextAreaField } from '@/mastodon/components/form_fields'; import { RangeInput } from '@/mastodon/components/form_fields/range_input_field'; import { selectImageInfo, @@ -22,6 +19,7 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import { DialogModal } from '../../ui/components/dialog_modal'; import type { DialogModalProps } from '../../ui/components/dialog_modal'; +import { ImageAltTextField } from './image_alt'; import classes from './styles.module.scss'; import 'react-easy-crop/react-easy-crop.css'; @@ -357,66 +355,19 @@ const StepAlt: FC<{ }> = ({ imageBlob, onCancel, onComplete }) => { const [altText, setAltText] = useState(''); - const handleChange: ChangeEventHandler = useCallback( - (event) => { - setAltText(event.currentTarget.value); - }, - [], - ); - const handleComplete = useCallback(() => { onComplete(altText); }, [altText, onComplete]); const imageSrc = useMemo(() => URL.createObjectURL(imageBlob), [imageBlob]); - const altLimit = useAppSelector( - (state) => - state.server.getIn( - ['server', 'configuration', 'media_attachments', 'description_limit'], - 150, - ) as number, - ); return ( <> - - -
    - - } - hint={ - - } - onChange={handleChange} - /> - -
    - - - } - > - - +