Skip to content

Commit 1492a48

Browse files
committed
Fix quote_id leaking into boost wrapper status
When boosting a quote post, the reblog endpoint spread the original post's fields into the new wrapper row, which caused quoteTargetId to be copied to the outer boost status. Clients like SubwayTooter would then see quote_id on the wrapper instead of only on the inner reblog object, breaking quote display. Fix by explicitly setting quoteTargetId: null when inserting the boost wrapper post, so quote_id and quote only appear inside the inner reblog object as expected. Also added a test case verifying that the outer wrapper has a null quote_id while the inner reblog retains the correct quote_id. Fixes #480 Assisted-by: Claude Code:claude-sonnet-4-6
1 parent 1becf2c commit 1492a48

3 files changed

Lines changed: 72 additions & 0 deletions

File tree

CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@ Version 0.8.4
66

77
To be released.
88

9+
- Fixed a bug where boosting a quote post would incorrectly copy the
10+
`quote_id` to the outer boost wrapper status, causing clients like
11+
SubwayTooter to fail to display the quoted post correctly. The
12+
`quote_id` and `quote` fields now only appear inside the inner `reblog`
13+
object. [[#480]]
14+
915
- Fixed a bug where no logs were output when running as a worker node
1016
(`NODE_TYPE=worker`). The logging system was only initialized when the
1117
web server started, so worker-only processes ran silently regardless of
1218
the `LOG_LEVEL` setting. [[#478]]
1319

1420
[#478]: https://github.com/fedify-dev/hollo/issues/478
21+
[#480]: https://github.com/fedify-dev/hollo/issues/480
1522

1623

1724
Version 0.8.3

src/api/v1/statuses.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,70 @@ describe.sequential("/api/v1/accounts/verify_credentials", () => {
214214
});
215215
});
216216

217+
describe.sequential("/api/v1/statuses/:id/reblog", () => {
218+
let client: Awaited<ReturnType<typeof createOAuthApplication>>;
219+
let account: Awaited<ReturnType<typeof createAccount>>;
220+
let accessToken: Awaited<ReturnType<typeof getAccessToken>>;
221+
222+
beforeEach(async () => {
223+
await cleanDatabase();
224+
225+
account = await createAccount({ generateKeyPair: true });
226+
client = await createOAuthApplication({ scopes: ["write:statuses"] });
227+
accessToken = await getAccessToken(client, account, ["write:statuses"]);
228+
});
229+
230+
it("does not carry quote_id on the boost wrapper when boosting a quote post", async () => {
231+
expect.assertions(5);
232+
233+
// Create the quoted post
234+
const quotedPostId = uuidv7();
235+
await db.insert(posts).values({
236+
id: quotedPostId,
237+
iri: `https://hollo.test/@hollo/${quotedPostId}`,
238+
type: "Note",
239+
accountId: account.id,
240+
visibility: "public",
241+
content: "Original post",
242+
contentHtml: "<p>Original post</p>",
243+
published: new Date(),
244+
});
245+
246+
// Create a quote post referencing the quoted post
247+
const quotePostId = uuidv7();
248+
await db.insert(posts).values({
249+
id: quotePostId,
250+
iri: `https://hollo.test/@hollo/${quotePostId}`,
251+
type: "Note",
252+
accountId: account.id,
253+
visibility: "public",
254+
content: "Quote post",
255+
contentHtml: "<p>Quote post</p>",
256+
quoteTargetId: quotedPostId,
257+
published: new Date(),
258+
});
259+
260+
// Boost the quote post
261+
const response = await app.request(
262+
`/api/v1/statuses/${quotePostId}/reblog`,
263+
{
264+
method: "POST",
265+
headers: { authorization: bearerAuthorization(accessToken) },
266+
},
267+
);
268+
269+
expect(response.status).toBe(200);
270+
const json = await response.json();
271+
272+
// The outer boost wrapper must not carry quote_id
273+
expect(json.quote_id).toBeNull();
274+
// The inner reblog object must retain the quote_id
275+
expect(json.reblog).not.toBeNull();
276+
expect(json.reblog.id).toBe(quotePostId);
277+
expect(json.reblog.quote_id).toBe(quotedPostId);
278+
});
279+
});
280+
217281
describe.sequential("/api/v1/statuses visibility", () => {
218282
let viewer: Awaited<ReturnType<typeof createAccount>>;
219283
let approvedAuthor: Awaited<ReturnType<typeof createAccount>>;

src/api/v1/statuses.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,7 @@ app.post(
845845
accountId: owner.id,
846846
applicationId: token.applicationId,
847847
replyTargetId: null,
848+
quoteTargetId: null,
848849
sharingId: originalPostId,
849850
visibility,
850851
url: url.href,

0 commit comments

Comments
 (0)