[mastodon-client] Respect idempotency-key for new posts

This commit is contained in:
Laura Hausmann 2023-09-29 23:29:14 +02:00
parent fe15584834
commit e0fefc986f
No known key found for this signature in database
GPG key ID: D044E84C5BE01605

View file

@ -13,11 +13,13 @@ import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
import { convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "@/server/api/mastodon/endpoints/timeline.js"; import { convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "@/server/api/mastodon/endpoints/timeline.js";
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
import { UserConverter } from "@/server/api/mastodon/converters/user.js"; import { UserConverter } from "@/server/api/mastodon/converters/user.js";
import { Cache } from "@/misc/cache.js";
import { redisClient } from "@/db/redis.js";
import AsyncLock from "async-lock";
import { ILocalUser } from "@/models/entities/user.js";
function normalizeQuery(data: any) { const postIdempotencyCache = new Cache<{status?: MastodonEntity.Status}>('postIdempotencyCache', 60 * 60);
const str = querystring.stringify(data); const postIdempotencyLocks = new AsyncLock();
return qs.parse(str);
}
export function setupEndpointsStatus(router: Router): void { export function setupEndpointsStatus(router: Router): void {
router.post("/v1/statuses", async (ctx) => { router.post("/v1/statuses", async (ctx) => {
@ -30,14 +32,26 @@ export function setupEndpointsStatus(router: Router): void {
return; return;
} }
const key = getIdempotencyKey(ctx.headers, user);
if (key !== null) {
const result = await getFromIdempotencyCache(key);
if (result) {
ctx.body = result;
return;
}
}
let request = NoteHelpers.normalizeComposeOptions(ctx.request.body); let request = NoteHelpers.normalizeComposeOptions(ctx.request.body);
const note = await NoteHelpers.createNote(request, user) ctx.body = await NoteHelpers.createNote(request, user)
.then(p => NoteConverter.encode(p, user)); .then(p => NoteConverter.encode(p, user))
ctx.body = convertStatus(note); .then(p => convertStatus(p));
if (key !== null) postIdempotencyCache.set(key, {status: ctx.body});
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 500;
ctx.body = e.response.data; ctx.body = { error: e.message };
} }
}); });
router.put("/v1/statuses/:id", async (ctx) => { router.put("/v1/statuses/:id", async (ctx) => {
@ -621,3 +635,32 @@ export function setupEndpointsStatus(router: Router): void {
}, },
); );
} }
function normalizeQuery(data: any) {
const str = querystring.stringify(data);
return qs.parse(str);
}
function getIdempotencyKey(headers: any, user: ILocalUser): string | null {
if (headers["idempotency-key"] === undefined || headers["idempotency-key"] === null) return null;
return `${user.id}-${Array.isArray(headers["idempotency-key"]) ? headers["idempotency-key"].at(-1)! : headers["idempotency-key"]}`;
}
async function getFromIdempotencyCache(key: string): Promise<MastodonEntity.Status | undefined> {
return postIdempotencyLocks.acquire(key, async (): Promise<MastodonEntity.Status | undefined> => {
if (await postIdempotencyCache.get(key) !== undefined) {
let i = 5;
while ((await postIdempotencyCache.get(key))?.status === undefined) {
if (++i > 5) throw new Error('Post is duplicate but unable to resolve original');
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
}
return (await postIdempotencyCache.get(key))?.status;
} else {
await postIdempotencyCache.set(key, {});
return undefined;
}
});
}