mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
refactor(api): ♻️ Use URL literal instead of strings
This commit is contained in:
parent
99fac323c8
commit
76d1ccc859
|
|
@ -69,7 +69,7 @@ export default apiRoute((app) =>
|
||||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${otherUser.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`,
|
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${otherUser.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`,
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
context.req.url,
|
new URL(context.req.url),
|
||||||
);
|
);
|
||||||
|
|
||||||
return context.json(
|
return context.json(
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ export default apiRoute((app) =>
|
||||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
|
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
|
||||||
),
|
),
|
||||||
context.req.valid("query").limit,
|
context.req.valid("query").limit,
|
||||||
context.req.url,
|
new URL(context.req.url),
|
||||||
);
|
);
|
||||||
|
|
||||||
return context.json(
|
return context.json(
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ beforeAll(async () => {
|
||||||
priority: 2,
|
priority: 2,
|
||||||
description: "test",
|
description: "test",
|
||||||
visible: true,
|
visible: true,
|
||||||
icon: "test",
|
icon: "https://test.com",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(role).toBeDefined();
|
expect(role).toBeDefined();
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ export default apiRoute((app) =>
|
||||||
exclude_replies ? isNull(Notes.replyId) : undefined,
|
exclude_replies ? isNull(Notes.replyId) : undefined,
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
context.req.url,
|
new URL(context.req.url),
|
||||||
user?.id,
|
user?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,11 @@ describe("/api/v1/accounts/update_credentials", () => {
|
||||||
const object = (await response.json()) as APIAccount;
|
const object = (await response.json()) as APIAccount;
|
||||||
// Proxy url is base_url/media/proxy/<base64url encoded url>
|
// Proxy url is base_url/media/proxy/<base64url encoded url>
|
||||||
expect(object.note).toBe(
|
expect(object.note).toBe(
|
||||||
`<p><img src="${config.http.base_url}/media/proxy/${Buffer.from(
|
`<p><img src="${config.http.base_url}media/proxy/${Buffer.from(
|
||||||
"https://example.com/image.jpg",
|
"https://example.com/image.jpg",
|
||||||
).toString("base64url")}"> <video src="${
|
).toString("base64url")}"> <video src="${
|
||||||
config.http.base_url
|
config.http.base_url
|
||||||
}/media/proxy/${Buffer.from(
|
}media/proxy/${Buffer.from(
|
||||||
"https://example.com/video.mp4",
|
"https://example.com/video.mp4",
|
||||||
).toString("base64url")}"> Test!</p>\n`,
|
).toString("base64url")}"> Test!</p>\n`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export default apiRoute((app) =>
|
||||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${user.id} AND "Relationships"."blocking" = true)`,
|
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${user.id} AND "Relationships"."blocking" = true)`,
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
context.req.url,
|
new URL(context.req.url),
|
||||||
);
|
);
|
||||||
|
|
||||||
return context.json(
|
return context.json(
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export default apiRoute((app) =>
|
||||||
sql`EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = ${Notes.id} AND "Likes"."likerId" = ${user.id})`,
|
sql`EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = ${Notes.id} AND "Likes"."likerId" = ${user.id})`,
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
context.req.url,
|
new URL(context.req.url),
|
||||||
user?.id,
|
user?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export default apiRoute((app) =>
|
||||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${user.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."requested" = true)`,
|
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${user.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."requested" = true)`,
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
context.req.url,
|
new URL(context.req.url),
|
||||||
);
|
);
|
||||||
|
|
||||||
return context.json(
|
return context.json(
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,12 @@ export default apiRoute((app) =>
|
||||||
status_count: statusCount,
|
status_count: statusCount,
|
||||||
user_count: userCount,
|
user_count: userCount,
|
||||||
},
|
},
|
||||||
thumbnail: proxyUrl(config.instance.logo),
|
thumbnail: config.instance.logo
|
||||||
banner: proxyUrl(config.instance.banner),
|
? proxyUrl(config.instance.logo).toString()
|
||||||
|
: null,
|
||||||
|
banner: config.instance.banner
|
||||||
|
? proxyUrl(config.instance.banner).toString()
|
||||||
|
: null,
|
||||||
title: config.instance.name,
|
title: config.instance.name,
|
||||||
uri: config.http.base_url,
|
uri: config.http.base_url,
|
||||||
urls: {
|
urls: {
|
||||||
|
|
@ -113,7 +117,9 @@ export default apiRoute((app) =>
|
||||||
providers:
|
providers:
|
||||||
oidcConfig?.providers?.map((p) => ({
|
oidcConfig?.providers?.map((p) => ({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
icon: proxyUrl(p.icon) || undefined,
|
icon: p.icon
|
||||||
|
? proxyUrl(new URL(p.icon)).toString()
|
||||||
|
: undefined,
|
||||||
id: p.id,
|
id: p.id,
|
||||||
})) ?? [],
|
})) ?? [],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export default apiRoute((app) =>
|
||||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${user.id} AND "Relationships"."muting" = true)`,
|
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${user.id} AND "Relationships"."muting" = true)`,
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
context.req.url,
|
new URL(context.req.url),
|
||||||
);
|
);
|
||||||
|
|
||||||
return context.json(
|
return context.json(
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ export default apiRoute((app) =>
|
||||||
)`,
|
)`,
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
context.req.url,
|
new URL(context.req.url),
|
||||||
user.id,
|
user.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ beforeAll(async () => {
|
||||||
priority: 2,
|
priority: 2,
|
||||||
description: "test",
|
description: "test",
|
||||||
visible: true,
|
visible: true,
|
||||||
icon: "test",
|
icon: "https://test.com",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(role).toBeDefined();
|
expect(role).toBeDefined();
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ beforeAll(async () => {
|
||||||
priority: 10,
|
priority: 10,
|
||||||
description: "test",
|
description: "test",
|
||||||
visible: true,
|
visible: true,
|
||||||
icon: "test",
|
icon: "https://test.com",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(role).toBeDefined();
|
expect(role).toBeDefined();
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export default apiRoute((app) =>
|
||||||
sql`EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = ${note.id} AND "Likes"."likerId" = ${Users.id})`,
|
sql`EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = ${note.id} AND "Likes"."likerId" = ${Users.id})`,
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
context.req.url,
|
new URL(context.req.url),
|
||||||
);
|
);
|
||||||
|
|
||||||
return context.json(
|
return context.json(
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export default apiRoute((app) =>
|
||||||
sql`EXISTS (SELECT 1 FROM "Notes" WHERE "Notes"."reblogId" = ${note.id} AND "Notes"."authorId" = ${Users.id})`,
|
sql`EXISTS (SELECT 1 FROM "Notes" WHERE "Notes"."reblogId" = ${note.id} AND "Notes"."authorId" = ${Users.id})`,
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
context.req.url,
|
new URL(context.req.url),
|
||||||
);
|
);
|
||||||
|
|
||||||
return context.json(
|
return context.json(
|
||||||
|
|
|
||||||
|
|
@ -429,11 +429,11 @@ describe("/api/v1/statuses", () => {
|
||||||
const object = (await response.json()) as ApiStatus;
|
const object = (await response.json()) as ApiStatus;
|
||||||
// Proxy url is base_url/media/proxy/<base64url encoded url>
|
// Proxy url is base_url/media/proxy/<base64url encoded url>
|
||||||
expect(object.content).toBe(
|
expect(object.content).toBe(
|
||||||
`<p><img src="${config.http.base_url}/media/proxy/${Buffer.from(
|
`<p><img src="${config.http.base_url}media/proxy/${Buffer.from(
|
||||||
"https://example.com/image.jpg",
|
"https://example.com/image.jpg",
|
||||||
).toString("base64url")}"> <video src="${
|
).toString("base64url")}"> <video src="${
|
||||||
config.http.base_url
|
config.http.base_url
|
||||||
}/media/proxy/${Buffer.from(
|
}media/proxy/${Buffer.from(
|
||||||
"https://example.com/video.mp4",
|
"https://example.com/video.mp4",
|
||||||
).toString("base64url")}"> Test!</p>`,
|
).toString("base64url")}"> Test!</p>`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ describe("/api/v1/timelines/home", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.headers.get("link")).toBe(
|
expect(response.headers.get("link")).toBe(
|
||||||
`<${config.http.base_url}/api/v1/timelines/home?limit=20&max_id=${timeline[19].id}>; rel="next"`,
|
`<${config.http.base_url}api/v1/timelines/home?limit=20&max_id=${timeline[19].id}>; rel="next"`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -112,7 +112,7 @@ describe("/api/v1/timelines/home", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.headers.get("link")).toInclude(
|
expect(response.headers.get("link")).toInclude(
|
||||||
`${config.http.base_url}/api/v1/timelines/home?limit=20&min_id=${timeline[20].id}>; rel="prev"`,
|
`${config.http.base_url}api/v1/timelines/home?limit=20&min_id=${timeline[20].id}>; rel="prev"`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export default apiRoute((app) =>
|
||||||
sql`NOT EXISTS (SELECT 1 FROM "Filters" WHERE "Filters"."userId" = ${user.id} AND "Filters"."filter_action" = 'hide' AND EXISTS (SELECT 1 FROM "FilterKeywords" WHERE "FilterKeywords"."filterId" = "Filters"."id" AND "Notes"."content" LIKE '%' || "FilterKeywords"."keyword" || '%') AND "Filters"."context" @> ARRAY['home'])`,
|
sql`NOT EXISTS (SELECT 1 FROM "Filters" WHERE "Filters"."userId" = ${user.id} AND "Filters"."filter_action" = 'hide' AND EXISTS (SELECT 1 FROM "FilterKeywords" WHERE "FilterKeywords"."filterId" = "Filters"."id" AND "Notes"."content" LIKE '%' || "FilterKeywords"."keyword" || '%') AND "Filters"."context" @> ARRAY['home'])`,
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
context.req.url,
|
new URL(context.req.url),
|
||||||
user.id,
|
user.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ describe("/api/v1/timelines/public", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.headers.get("link")).toBe(
|
expect(response.headers.get("link")).toBe(
|
||||||
`<${config.http.base_url}/api/v1/timelines/public?limit=20&max_id=${timeline[19].id}>; rel="next"`,
|
`<${config.http.base_url}api/v1/timelines/public?limit=20&max_id=${timeline[19].id}>; rel="next"`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -153,7 +153,7 @@ describe("/api/v1/timelines/public", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.headers.get("link")).toInclude(
|
expect(response.headers.get("link")).toInclude(
|
||||||
`${config.http.base_url}/api/v1/timelines/public?limit=20&min_id=${timeline[20].id}>; rel="prev"`,
|
`${config.http.base_url}api/v1/timelines/public?limit=20&min_id=${timeline[20].id}>; rel="prev"`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export default apiRoute((app) =>
|
||||||
: eq(Notes.visibility, "public"),
|
: eq(Notes.visibility, "public"),
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
context.req.url,
|
new URL(context.req.url),
|
||||||
user?.id,
|
user?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,10 +147,14 @@ export default apiRoute((app) =>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
url: proxyUrl(config.instance.logo),
|
url: config.instance.logo
|
||||||
|
? proxyUrl(config.instance.logo)
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
banner: {
|
banner: {
|
||||||
url: proxyUrl(config.instance.banner),
|
url: config.instance.banner
|
||||||
|
? proxyUrl(config.instance.banner)
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
languages: ["en"],
|
languages: ["en"],
|
||||||
configuration: {
|
configuration: {
|
||||||
|
|
@ -227,7 +231,7 @@ export default apiRoute((app) =>
|
||||||
providers:
|
providers:
|
||||||
oidcConfig?.providers?.map((p) => ({
|
oidcConfig?.providers?.map((p) => ({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
icon: proxyUrl(p.icon) ?? "",
|
icon: p.icon ? proxyUrl(new URL(p.icon)) : "",
|
||||||
id: p.id,
|
id: p.id,
|
||||||
})) ?? [],
|
})) ?? [],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,11 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
const userJson = user.toVersia();
|
const userJson = user.toVersia();
|
||||||
|
|
||||||
const { headers } = await user.sign(userJson, context.req.url, "GET");
|
const { headers } = await user.sign(
|
||||||
|
userJson,
|
||||||
|
new URL(context.req.url),
|
||||||
|
"GET",
|
||||||
|
);
|
||||||
|
|
||||||
return context.json(userJson, 200, headers.toJSON());
|
return context.json(userJson, 200, headers.toJSON());
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ export default apiRoute((app) =>
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
).toString(),
|
).toString(),
|
||||||
total: totalNotes,
|
total: totalNotes,
|
||||||
author: author.getUri(),
|
author: author.getUri().toString(),
|
||||||
next:
|
next:
|
||||||
notes.length === NOTES_PER_PAGE
|
notes.length === NOTES_PER_PAGE
|
||||||
? new URL(
|
? new URL(
|
||||||
|
|
@ -125,7 +125,11 @@ export default apiRoute((app) =>
|
||||||
items: notes.map((note) => note.toVersia()),
|
items: notes.map((note) => note.toVersia()),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { headers } = await author.sign(json, context.req.url, "GET");
|
const { headers } = await author.sign(
|
||||||
|
json,
|
||||||
|
new URL(context.req.url),
|
||||||
|
"GET",
|
||||||
|
);
|
||||||
|
|
||||||
return context.json(json, 200, headers.toJSON());
|
return context.json(json, 200, headers.toJSON());
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ export default apiRoute((app) =>
|
||||||
user.data.username,
|
user.data.username,
|
||||||
new URL(config.http.base_url).host,
|
new URL(config.http.base_url).host,
|
||||||
"application/activity+json",
|
"application/activity+json",
|
||||||
config.federation.bridge.url,
|
config.federation.bridge.url?.toString(),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e as ResponseError;
|
const error = e as ResponseError;
|
||||||
|
|
|
||||||
|
|
@ -188,8 +188,8 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
shortcode: this.data.shortcode,
|
shortcode: this.data.shortcode,
|
||||||
static_url: proxyUrl(this.media.getUrl()) ?? "", // TODO: Add static version
|
static_url: proxyUrl(this.media.getUrl()).toString(),
|
||||||
url: proxyUrl(this.media.getUrl()) ?? "",
|
url: proxyUrl(this.media.getUrl()).toString(),
|
||||||
visible_in_picker: this.data.visibleInPicker,
|
visible_in_picker: this.data.visibleInPicker,
|
||||||
category: this.data.category ?? undefined,
|
category: this.data.category ?? undefined,
|
||||||
global: this.data.ownerId === null,
|
global: this.data.ownerId === null,
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
||||||
return this.data.id;
|
return this.data.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async fetchMetadata(url: string): Promise<{
|
public static async fetchMetadata(url: URL): Promise<{
|
||||||
metadata: InstanceMetadata;
|
metadata: InstanceMetadata;
|
||||||
protocol: "versia" | "activitypub";
|
protocol: "versia" | "activitypub";
|
||||||
}> {
|
}> {
|
||||||
|
|
@ -184,7 +184,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async fetchActivityPubMetadata(
|
private static async fetchActivityPubMetadata(
|
||||||
url: string,
|
url: URL,
|
||||||
): Promise<InstanceMetadata | null> {
|
): Promise<InstanceMetadata | null> {
|
||||||
const origin = new URL(url).origin;
|
const origin = new URL(url).origin;
|
||||||
const wellKnownUrl = new URL("/.well-known/nodeinfo", origin);
|
const wellKnownUrl = new URL("/.well-known/nodeinfo", origin);
|
||||||
|
|
@ -311,18 +311,18 @@ export class Instance extends BaseInterface<typeof Instances> {
|
||||||
|
|
||||||
public static resolveFromHost(host: string): Promise<Instance> {
|
public static resolveFromHost(host: string): Promise<Instance> {
|
||||||
if (host.startsWith("http")) {
|
if (host.startsWith("http")) {
|
||||||
const url = new URL(host).host;
|
const url = new URL(host);
|
||||||
|
|
||||||
return Instance.resolve(url);
|
return Instance.resolve(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(`https://${host}`);
|
const url = new URL(`https://${host}`);
|
||||||
|
|
||||||
return Instance.resolve(url.origin);
|
return Instance.resolve(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async resolve(url: string): Promise<Instance> {
|
public static async resolve(url: URL): Promise<Instance> {
|
||||||
const host = new URL(url).host;
|
const host = url.host;
|
||||||
|
|
||||||
const existingInstance = await Instance.fromSql(
|
const existingInstance = await Instance.fromSql(
|
||||||
eq(Instances.baseUrl, host),
|
eq(Instances.baseUrl, host),
|
||||||
|
|
@ -352,7 +352,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
||||||
const logger = getLogger(["federation", "resolvers"]);
|
const logger = getLogger(["federation", "resolvers"]);
|
||||||
|
|
||||||
const output = await Instance.fetchMetadata(
|
const output = await Instance.fetchMetadata(
|
||||||
`https://${this.data.baseUrl}`,
|
new URL(`https://${this.data.baseUrl}`),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!output) {
|
if (!output) {
|
||||||
|
|
|
||||||
|
|
@ -167,15 +167,17 @@ export class Like extends BaseInterface<typeof Likes, LikeType> {
|
||||||
id: this.data.id,
|
id: this.data.id,
|
||||||
author: User.getUri(
|
author: User.getUri(
|
||||||
this.data.liker.id,
|
this.data.liker.id,
|
||||||
this.data.liker.uri,
|
this.data.liker.uri ? new URL(this.data.liker.uri) : null,
|
||||||
config.http.base_url,
|
).toString(),
|
||||||
),
|
|
||||||
type: "pub.versia:likes/Like",
|
type: "pub.versia:likes/Like",
|
||||||
created_at: new Date(this.data.createdAt).toISOString(),
|
created_at: new Date(this.data.createdAt).toISOString(),
|
||||||
liked: Note.getUri(
|
liked:
|
||||||
|
Note.getUri(
|
||||||
this.data.liked.id,
|
this.data.liked.id,
|
||||||
this.data.liked.uri,
|
this.data.liked.uri
|
||||||
) as string,
|
? new URL(this.data.liked.uri)
|
||||||
|
: undefined,
|
||||||
|
)?.toString() ?? "",
|
||||||
uri: this.getUri().toString(),
|
uri: this.getUri().toString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -187,9 +189,12 @@ export class Like extends BaseInterface<typeof Likes, LikeType> {
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
author: User.getUri(
|
author: User.getUri(
|
||||||
unliker?.id ?? this.data.liker.id,
|
unliker?.id ?? this.data.liker.id,
|
||||||
unliker?.data.uri ?? this.data.liker.uri,
|
unliker?.data.uri
|
||||||
config.http.base_url,
|
? new URL(unliker.data.uri)
|
||||||
),
|
: this.data.liker.uri
|
||||||
|
? new URL(this.data.liker.uri)
|
||||||
|
: null,
|
||||||
|
).toString(),
|
||||||
deleted_type: "pub.versia:likes/Like",
|
deleted_type: "pub.versia:likes/Like",
|
||||||
deleted: this.getUri().toString(),
|
deleted: this.getUri().toString(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ export class Media extends BaseInterface<typeof Medias> {
|
||||||
|
|
||||||
const url = Media.getUrl(path);
|
const url = Media.getUrl(path);
|
||||||
|
|
||||||
let thumbnailUrl = "";
|
let thumbnailUrl: URL | null = null;
|
||||||
|
|
||||||
if (options?.thumbnail) {
|
if (options?.thumbnail) {
|
||||||
const { path } = await Media.upload(options.thumbnail);
|
const { path } = await Media.upload(options.thumbnail);
|
||||||
|
|
@ -220,10 +220,15 @@ export class Media extends BaseInterface<typeof Medias> {
|
||||||
const content = await Media.fileToContentFormat(file, url, {
|
const content = await Media.fileToContentFormat(file, url, {
|
||||||
description: options?.description,
|
description: options?.description,
|
||||||
});
|
});
|
||||||
const thumbnailContent = options?.thumbnail
|
const thumbnailContent =
|
||||||
? await Media.fileToContentFormat(options.thumbnail, thumbnailUrl, {
|
thumbnailUrl && options?.thumbnail
|
||||||
|
? await Media.fileToContentFormat(
|
||||||
|
options.thumbnail,
|
||||||
|
thumbnailUrl,
|
||||||
|
{
|
||||||
description: options?.description,
|
description: options?.description,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const newAttachment = await Media.insert({
|
const newAttachment = await Media.insert({
|
||||||
|
|
@ -246,6 +251,12 @@ export class Media extends BaseInterface<typeof Medias> {
|
||||||
return newAttachment;
|
return newAttachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and adds a new media attachment from a URL
|
||||||
|
* @param uri
|
||||||
|
* @param options
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
public static async fromUrl(
|
public static async fromUrl(
|
||||||
uri: URL,
|
uri: URL,
|
||||||
options?: {
|
options?: {
|
||||||
|
|
@ -377,20 +388,21 @@ export class Media extends BaseInterface<typeof Medias> {
|
||||||
return this.data.id;
|
return this.data.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getUrl(name: string): string {
|
public static getUrl(name: string): URL {
|
||||||
if (config.media.backend === MediaBackendType.Local) {
|
if (config.media.backend === MediaBackendType.Local) {
|
||||||
return new URL(`/media/${name}`, config.http.base_url).toString();
|
return new URL(`/media/${name}`, config.http.base_url);
|
||||||
}
|
}
|
||||||
if (config.media.backend === MediaBackendType.S3) {
|
if (config.media.backend === MediaBackendType.S3) {
|
||||||
return new URL(`/${name}`, config.s3?.public_url).toString();
|
return new URL(`/${name}`, config.s3?.public_url);
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUrl(): string {
|
throw new Error("Unknown media backend");
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUrl(): URL {
|
||||||
const type = this.getPreferredMimeType();
|
const type = this.getPreferredMimeType();
|
||||||
|
|
||||||
return this.data.content[type]?.content;
|
return new URL(this.data.content[type]?.content ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -461,7 +473,7 @@ export class Media extends BaseInterface<typeof Medias> {
|
||||||
*/
|
*/
|
||||||
public static async fileToContentFormat(
|
public static async fileToContentFormat(
|
||||||
file: File,
|
file: File,
|
||||||
uri: string,
|
uri: URL,
|
||||||
options?: Partial<{
|
options?: Partial<{
|
||||||
description: string;
|
description: string;
|
||||||
}>,
|
}>,
|
||||||
|
|
@ -475,7 +487,7 @@ export class Media extends BaseInterface<typeof Medias> {
|
||||||
// Thumbhash should be added in a worker after the file is uploaded
|
// Thumbhash should be added in a worker after the file is uploaded
|
||||||
return {
|
return {
|
||||||
[file.type]: {
|
[file.type]: {
|
||||||
content: uri,
|
content: uri.toString(),
|
||||||
remote: true,
|
remote: true,
|
||||||
hash: {
|
hash: {
|
||||||
sha256: hash,
|
sha256: hash,
|
||||||
|
|
@ -528,9 +540,11 @@ export class Media extends BaseInterface<typeof Medias> {
|
||||||
return {
|
return {
|
||||||
id: this.data.id,
|
id: this.data.id,
|
||||||
type: this.getMastodonType(),
|
type: this.getMastodonType(),
|
||||||
url: proxyUrl(data.content) ?? "",
|
url: proxyUrl(new URL(data.content)).toString(),
|
||||||
remote_url: null,
|
remote_url: null,
|
||||||
preview_url: proxyUrl(thumbnailData?.content),
|
preview_url: thumbnailData?.content
|
||||||
|
? proxyUrl(new URL(thumbnailData.content)).toString()
|
||||||
|
: null,
|
||||||
text_url: null,
|
text_url: null,
|
||||||
meta: this.toApiMeta(),
|
meta: this.toApiMeta(),
|
||||||
description: data.description || null,
|
description: data.description || null,
|
||||||
|
|
|
||||||
|
|
@ -646,17 +646,17 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
* @param uri - The URI of the note to resolve
|
* @param uri - The URI of the note to resolve
|
||||||
* @returns The resolved note
|
* @returns The resolved note
|
||||||
*/
|
*/
|
||||||
public static async resolve(uri: string): Promise<Note | null> {
|
public static async resolve(uri: URL): Promise<Note | null> {
|
||||||
// Check if note not already in database
|
// Check if note not already in database
|
||||||
const foundNote = await Note.fromSql(eq(Notes.uri, uri));
|
const foundNote = await Note.fromSql(eq(Notes.uri, uri.toString()));
|
||||||
|
|
||||||
if (foundNote) {
|
if (foundNote) {
|
||||||
return foundNote;
|
return foundNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if URI is of a local note
|
// Check if URI is of a local note
|
||||||
if (uri.startsWith(config.http.base_url)) {
|
if (uri.origin === config.http.base_url.origin) {
|
||||||
const uuid = uri.match(idValidator);
|
const uuid = uri.pathname.match(idValidator);
|
||||||
|
|
||||||
if (!uuid?.[0]) {
|
if (!uuid?.[0]) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -675,7 +675,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
* @param uri - The URI of the note to save
|
* @param uri - The URI of the note to save
|
||||||
* @returns The saved note, or null if the note could not be fetched
|
* @returns The saved note, or null if the note could not be fetched
|
||||||
*/
|
*/
|
||||||
public static async fetchFromRemote(uri: string): Promise<Note | null> {
|
public static async fetchFromRemote(uri: URL): Promise<Note | null> {
|
||||||
const instance = await Instance.resolve(uri);
|
const instance = await Instance.resolve(uri);
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
|
|
@ -691,7 +691,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
|
|
||||||
const note = await new EntityValidator().Note(data);
|
const note = await new EntityValidator().Note(data);
|
||||||
|
|
||||||
const author = await User.resolve(note.author);
|
const author = await User.resolve(new URL(note.author));
|
||||||
|
|
||||||
if (!author) {
|
if (!author) {
|
||||||
throw new Error("Invalid object author");
|
throw new Error("Invalid object author");
|
||||||
|
|
@ -773,15 +773,15 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
uri: note.uri,
|
uri: note.uri,
|
||||||
mentions: await Promise.all(
|
mentions: await Promise.all(
|
||||||
(note.mentions ?? [])
|
(note.mentions ?? [])
|
||||||
.map((mention) => User.resolve(mention))
|
.map((mention) => User.resolve(new URL(mention)))
|
||||||
.filter((mention) => mention !== null) as Promise<User>[],
|
.filter((mention) => mention !== null) as Promise<User>[],
|
||||||
),
|
),
|
||||||
mediaAttachments: attachments,
|
mediaAttachments: attachments,
|
||||||
replyId: note.replies_to
|
replyId: note.replies_to
|
||||||
? (await Note.resolve(note.replies_to))?.data.id
|
? (await Note.resolve(new URL(note.replies_to)))?.data.id
|
||||||
: undefined,
|
: undefined,
|
||||||
quoteId: note.quotes
|
quoteId: note.quotes
|
||||||
? (await Note.resolve(note.quotes))?.data.id
|
? (await Note.resolve(new URL(note.quotes)))?.data.id
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -908,7 +908,10 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
mention.username,
|
mention.username,
|
||||||
mention.instance?.baseUrl,
|
mention.instance?.baseUrl,
|
||||||
),
|
),
|
||||||
url: User.getUri(mention.id, mention.uri, config.http.base_url),
|
url: User.getUri(
|
||||||
|
mention.id,
|
||||||
|
mention.uri ? new URL(mention.uri) : null,
|
||||||
|
).toString(),
|
||||||
username: mention.username,
|
username: mention.username,
|
||||||
})),
|
})),
|
||||||
language: null,
|
language: null,
|
||||||
|
|
@ -927,9 +930,9 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
sensitive: data.sensitive,
|
sensitive: data.sensitive,
|
||||||
spoiler_text: data.spoilerText,
|
spoiler_text: data.spoilerText,
|
||||||
tags: [],
|
tags: [],
|
||||||
uri: data.uri || this.getUri(),
|
uri: data.uri || this.getUri().toString(),
|
||||||
visibility: data.visibility as ApiStatus["visibility"],
|
visibility: data.visibility as ApiStatus["visibility"],
|
||||||
url: data.uri || this.getMastoUri(),
|
url: data.uri || this.getMastoUri().toString(),
|
||||||
bookmarked: false,
|
bookmarked: false,
|
||||||
quote: data.quotingId
|
quote: data.quotingId
|
||||||
? ((await Note.fromId(data.quotingId, userFetching?.id).then(
|
? ((await Note.fromId(data.quotingId, userFetching?.id).then(
|
||||||
|
|
@ -944,14 +947,11 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUri(): string {
|
public getUri(): URL {
|
||||||
return this.data.uri || localObjectUri(this.id);
|
return new URL(this.data.uri || localObjectUri(this.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getUri(
|
public static getUri(id: string | null, uri?: URL | null): URL | null {
|
||||||
id: string | null,
|
|
||||||
uri?: string | null,
|
|
||||||
): string | null {
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -962,11 +962,11 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
* Get the frontend URI of this note
|
* Get the frontend URI of this note
|
||||||
* @returns The frontend URI of this note
|
* @returns The frontend URI of this note
|
||||||
*/
|
*/
|
||||||
public getMastoUri(): string {
|
public getMastoUri(): URL {
|
||||||
return new URL(
|
return new URL(
|
||||||
`/@${this.author.data.username}/${this.id}`,
|
`/@${this.author.data.username}/${this.id}`,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
).toString();
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteToVersia(): VersiaDelete {
|
public deleteToVersia(): VersiaDelete {
|
||||||
|
|
@ -975,9 +975,9 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
return {
|
return {
|
||||||
type: "Delete",
|
type: "Delete",
|
||||||
id,
|
id,
|
||||||
author: this.author.getUri(),
|
author: this.author.getUri().toString(),
|
||||||
deleted_type: "Note",
|
deleted_type: "Note",
|
||||||
deleted: this.getUri(),
|
deleted: this.getUri().toString(),
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -992,8 +992,8 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
type: "Note",
|
type: "Note",
|
||||||
created_at: new Date(status.createdAt).toISOString(),
|
created_at: new Date(status.createdAt).toISOString(),
|
||||||
id: status.id,
|
id: status.id,
|
||||||
author: this.author.getUri(),
|
author: this.author.getUri().toString(),
|
||||||
uri: this.getUri(),
|
uri: this.getUri().toString(),
|
||||||
content: {
|
content: {
|
||||||
"text/html": {
|
"text/html": {
|
||||||
content: status.content,
|
content: status.content,
|
||||||
|
|
@ -1009,12 +1009,19 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
),
|
),
|
||||||
is_sensitive: status.sensitive,
|
is_sensitive: status.sensitive,
|
||||||
mentions: status.mentions.map((mention) =>
|
mentions: status.mentions.map((mention) =>
|
||||||
User.getUri(mention.id, mention.uri, config.http.base_url),
|
User.getUri(
|
||||||
|
mention.id,
|
||||||
|
mention.uri ? new URL(mention.uri) : null,
|
||||||
|
).toString(),
|
||||||
),
|
),
|
||||||
quotes:
|
quotes: Note.getUri(
|
||||||
Note.getUri(status.quotingId, status.quote?.uri) ?? undefined,
|
status.quotingId,
|
||||||
replies_to:
|
status.quote?.uri ? new URL(status.quote.uri) : null,
|
||||||
Note.getUri(status.replyId, status.reply?.uri) ?? undefined,
|
)?.toString(),
|
||||||
|
replies_to: Note.getUri(
|
||||||
|
status.replyId,
|
||||||
|
status.reply?.uri ? new URL(status.reply.uri) : null,
|
||||||
|
)?.toString(),
|
||||||
subject: status.spoilerText,
|
subject: status.spoilerText,
|
||||||
// TODO: Refactor as part of groups
|
// TODO: Refactor as part of groups
|
||||||
group: status.visibility === "public" ? "public" : "followers",
|
group: status.visibility === "public" ? "public" : "followers",
|
||||||
|
|
|
||||||
|
|
@ -179,13 +179,12 @@ export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUri(baseUrl: string): string {
|
public getUri(baseUrl: URL): URL {
|
||||||
return (
|
return this.data.uri
|
||||||
this.data.uri ||
|
? new URL(this.data.uri)
|
||||||
new URL(
|
: new URL(
|
||||||
`/objects/${this.data.noteId}/reactions/${this.id}`,
|
`/objects/${this.data.noteId}/reactions/${this.id}`,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
).toString()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,19 +202,21 @@ export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uri: this.getUri(config.http.base_url),
|
uri: this.getUri(config.http.base_url).toString(),
|
||||||
type: "pub.versia:reactions/Reaction",
|
type: "pub.versia:reactions/Reaction",
|
||||||
author: User.getUri(
|
author: User.getUri(
|
||||||
this.data.authorId,
|
this.data.authorId,
|
||||||
this.data.author.uri,
|
this.data.author.uri ? new URL(this.data.author.uri) : null,
|
||||||
config.http.base_url,
|
).toString(),
|
||||||
),
|
|
||||||
created_at: new Date(this.data.createdAt).toISOString(),
|
created_at: new Date(this.data.createdAt).toISOString(),
|
||||||
id: this.id,
|
id: this.id,
|
||||||
object: Note.getUri(
|
object:
|
||||||
|
Note.getUri(
|
||||||
this.data.note.id,
|
this.data.note.id,
|
||||||
this.data.note.uri,
|
this.data.note.uri
|
||||||
) as string,
|
? new URL(this.data.note.uri)
|
||||||
|
: undefined,
|
||||||
|
)?.toString() ?? "",
|
||||||
content: this.hasCustomEmoji()
|
content: this.hasCustomEmoji()
|
||||||
? `:${this.data.emoji?.shortcode}:`
|
? `:${this.data.emoji?.shortcode}:`
|
||||||
: this.data.emojiText || "",
|
: this.data.emojiText || "",
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,9 @@ export class Role extends BaseInterface<typeof Roles> {
|
||||||
priority: this.data.priority,
|
priority: this.data.priority,
|
||||||
description: this.data.description ?? undefined,
|
description: this.data.description ?? undefined,
|
||||||
visible: this.data.visible,
|
visible: this.data.visible,
|
||||||
icon: proxyUrl(this.data.icon) ?? undefined,
|
icon: this.data.icon
|
||||||
|
? proxyUrl(new URL(this.data.icon)).toString()
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export class Timeline<Type extends Note | User | Notification> {
|
||||||
public static getNoteTimeline(
|
public static getNoteTimeline(
|
||||||
sql: SQL<unknown> | undefined,
|
sql: SQL<unknown> | undefined,
|
||||||
limit: number,
|
limit: number,
|
||||||
url: string,
|
url: URL,
|
||||||
userId?: string,
|
userId?: string,
|
||||||
): Promise<{ link: string; objects: Note[] }> {
|
): Promise<{ link: string; objects: Note[] }> {
|
||||||
return new Timeline<Note>(TimelineType.Note).fetchTimeline(
|
return new Timeline<Note>(TimelineType.Note).fetchTimeline(
|
||||||
|
|
@ -31,7 +31,7 @@ export class Timeline<Type extends Note | User | Notification> {
|
||||||
public static getUserTimeline(
|
public static getUserTimeline(
|
||||||
sql: SQL<unknown> | undefined,
|
sql: SQL<unknown> | undefined,
|
||||||
limit: number,
|
limit: number,
|
||||||
url: string,
|
url: URL,
|
||||||
): Promise<{ link: string; objects: User[] }> {
|
): Promise<{ link: string; objects: User[] }> {
|
||||||
return new Timeline<User>(TimelineType.User).fetchTimeline(
|
return new Timeline<User>(TimelineType.User).fetchTimeline(
|
||||||
sql,
|
sql,
|
||||||
|
|
@ -43,7 +43,7 @@ export class Timeline<Type extends Note | User | Notification> {
|
||||||
public static getNotificationTimeline(
|
public static getNotificationTimeline(
|
||||||
sql: SQL<unknown> | undefined,
|
sql: SQL<unknown> | undefined,
|
||||||
limit: number,
|
limit: number,
|
||||||
url: string,
|
url: URL,
|
||||||
userId?: string,
|
userId?: string,
|
||||||
): Promise<{ link: string; objects: Notification[] }> {
|
): Promise<{ link: string; objects: Notification[] }> {
|
||||||
return new Timeline<Notification>(
|
return new Timeline<Notification>(
|
||||||
|
|
@ -85,14 +85,11 @@ export class Timeline<Type extends Note | User | Notification> {
|
||||||
|
|
||||||
private async fetchLinkHeader(
|
private async fetchLinkHeader(
|
||||||
objects: Type[],
|
objects: Type[],
|
||||||
url: string,
|
url: URL,
|
||||||
limit: number,
|
limit: number,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const linkHeader: string[] = [];
|
const linkHeader: string[] = [];
|
||||||
const urlWithoutQuery = new URL(
|
const urlWithoutQuery = new URL(url.pathname, config.http.base_url);
|
||||||
new URL(url).pathname,
|
|
||||||
config.http.base_url,
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
if (objects.length > 0) {
|
if (objects.length > 0) {
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
|
|
@ -130,7 +127,7 @@ export class Timeline<Type extends Note | User | Notification> {
|
||||||
|
|
||||||
private static async fetchNoteLinkHeader(
|
private static async fetchNoteLinkHeader(
|
||||||
notes: Note[],
|
notes: Note[],
|
||||||
urlWithoutQuery: string,
|
urlWithoutQuery: URL,
|
||||||
limit: number,
|
limit: number,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const linkHeader: string[] = [];
|
const linkHeader: string[] = [];
|
||||||
|
|
@ -158,7 +155,7 @@ export class Timeline<Type extends Note | User | Notification> {
|
||||||
|
|
||||||
private static async fetchUserLinkHeader(
|
private static async fetchUserLinkHeader(
|
||||||
users: User[],
|
users: User[],
|
||||||
urlWithoutQuery: string,
|
urlWithoutQuery: URL,
|
||||||
limit: number,
|
limit: number,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const linkHeader: string[] = [];
|
const linkHeader: string[] = [];
|
||||||
|
|
@ -186,7 +183,7 @@ export class Timeline<Type extends Note | User | Notification> {
|
||||||
|
|
||||||
private static async fetchNotificationLinkHeader(
|
private static async fetchNotificationLinkHeader(
|
||||||
notifications: Notification[],
|
notifications: Notification[],
|
||||||
urlWithoutQuery: string,
|
urlWithoutQuery: URL,
|
||||||
limit: number,
|
limit: number,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const linkHeader: string[] = [];
|
const linkHeader: string[] = [];
|
||||||
|
|
@ -220,7 +217,7 @@ export class Timeline<Type extends Note | User | Notification> {
|
||||||
private async fetchTimeline(
|
private async fetchTimeline(
|
||||||
sql: SQL<unknown> | undefined,
|
sql: SQL<unknown> | undefined,
|
||||||
limit: number,
|
limit: number,
|
||||||
url: string,
|
url: URL,
|
||||||
userId?: string,
|
userId?: string,
|
||||||
): Promise<{ link: string; objects: Type[] }> {
|
): Promise<{ link: string; objects: Type[] }> {
|
||||||
const objects = await this.fetchObjects(sql, limit, userId);
|
const objects = await this.fetchObjects(sql, limit, userId);
|
||||||
|
|
|
||||||
|
|
@ -228,19 +228,14 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
return !this.isLocal();
|
return !this.isLocal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUri(): string {
|
public getUri(): URL {
|
||||||
return (
|
return this.data.uri
|
||||||
this.data.uri ||
|
? new URL(this.data.uri)
|
||||||
new URL(`/users/${this.data.id}`, config.http.base_url).toString()
|
: new URL(`/users/${this.data.id}`, config.http.base_url);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getUri(
|
public static getUri(id: string, uri: URL | null): URL {
|
||||||
id: string,
|
return uri ? uri : new URL(`/users/${id}`, config.http.base_url);
|
||||||
uri: string | null,
|
|
||||||
baseUrl: string,
|
|
||||||
): string {
|
|
||||||
return uri || new URL(`/users/${id}`, baseUrl).toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasPermission(permission: RolePermissions): boolean {
|
public hasPermission(permission: RolePermissions): boolean {
|
||||||
|
|
@ -290,8 +285,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
entity: {
|
entity: {
|
||||||
type: "Follow",
|
type: "Follow",
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
author: this.getUri(),
|
author: this.getUri().toString(),
|
||||||
followee: otherUser.getUri(),
|
followee: otherUser.getUri().toString(),
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
recipientId: otherUser.id,
|
recipientId: otherUser.id,
|
||||||
|
|
@ -329,9 +324,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
return {
|
return {
|
||||||
type: "Unfollow",
|
type: "Unfollow",
|
||||||
id,
|
id,
|
||||||
author: this.getUri(),
|
author: this.getUri().toString(),
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
followee: followee.getUri(),
|
followee: followee.getUri().toString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -347,9 +342,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
const entity: VersiaFollowAccept = {
|
const entity: VersiaFollowAccept = {
|
||||||
type: "FollowAccept",
|
type: "FollowAccept",
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
author: this.getUri(),
|
author: this.getUri().toString(),
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
follower: follower.getUri(),
|
follower: follower.getUri().toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
await deliveryQueue.add(DeliveryJobType.FederateEntity, {
|
await deliveryQueue.add(DeliveryJobType.FederateEntity, {
|
||||||
|
|
@ -371,9 +366,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
const entity: VersiaFollowReject = {
|
const entity: VersiaFollowReject = {
|
||||||
type: "FollowReject",
|
type: "FollowReject",
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
author: this.getUri(),
|
author: this.getUri().toString(),
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
follower: follower.getUri(),
|
follower: follower.getUri().toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
await deliveryQueue.add(DeliveryJobType.FederateEntity, {
|
await deliveryQueue.add(DeliveryJobType.FederateEntity, {
|
||||||
|
|
@ -390,19 +385,21 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
* @param hostname
|
* @param hostname
|
||||||
* @returns URI, or null if not found
|
* @returns URI, or null if not found
|
||||||
*/
|
*/
|
||||||
public static webFinger(
|
public static async webFinger(
|
||||||
manager: FederationRequester,
|
manager: FederationRequester,
|
||||||
username: string,
|
username: string,
|
||||||
hostname: string,
|
hostname: string,
|
||||||
): Promise<string | null> {
|
): Promise<URL | null> {
|
||||||
try {
|
try {
|
||||||
return manager.webFinger(username, hostname);
|
return new URL(await manager.webFinger(username, hostname));
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
try {
|
||||||
return manager.webFinger(
|
return new URL(
|
||||||
|
await manager.webFinger(
|
||||||
username,
|
username,
|
||||||
hostname,
|
hostname,
|
||||||
"application/activity+json",
|
"application/activity+json",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
|
|
@ -511,7 +508,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
id: issuer.id,
|
id: issuer.id,
|
||||||
name: issuer.name,
|
name: issuer.name,
|
||||||
url: issuer.url,
|
url: issuer.url,
|
||||||
icon: proxyUrl(issuer.icon) || undefined,
|
icon: issuer.icon
|
||||||
|
? proxyUrl(new URL(issuer.icon)).toString()
|
||||||
|
: undefined,
|
||||||
server_id: account.serverId,
|
server_id: account.serverId,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
@ -662,11 +661,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async fetchFromRemote(uri: string): Promise<User | null> {
|
public static async fetchFromRemote(uri: URL): Promise<User | null> {
|
||||||
if (!URL.canParse(uri)) {
|
|
||||||
throw new Error(`Invalid URI: ${uri}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = await Instance.resolve(uri);
|
const instance = await Instance.resolve(uri);
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
|
|
@ -684,19 +679,19 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
|
|
||||||
const bridgeUri = new URL(
|
const bridgeUri = new URL(
|
||||||
`/apbridge/versia/query?${new URLSearchParams({
|
`/apbridge/versia/query?${new URLSearchParams({
|
||||||
user_url: uri,
|
user_url: uri.toString(),
|
||||||
})}`,
|
})}`,
|
||||||
config.federation.bridge.url,
|
config.federation.bridge.url,
|
||||||
);
|
);
|
||||||
|
|
||||||
return await User.saveFromVersia(bridgeUri.toString(), instance);
|
return await User.saveFromVersia(bridgeUri, instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Unsupported protocol: ${instance.data.protocol}`);
|
throw new Error(`Unsupported protocol: ${instance.data.protocol}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async saveFromVersia(
|
private static async saveFromVersia(
|
||||||
uri: string,
|
uri: URL,
|
||||||
instance: Instance,
|
instance: Instance,
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
const requester = await User.getFederationRequester();
|
const requester = await User.getFederationRequester();
|
||||||
|
|
@ -859,19 +854,19 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async resolve(uri: string): Promise<User | null> {
|
public static async resolve(uri: URL): Promise<User | null> {
|
||||||
getLogger(["federation", "resolvers"])
|
getLogger(["federation", "resolvers"])
|
||||||
.debug`Resolving user ${chalk.gray(uri)}`;
|
.debug`Resolving user ${chalk.gray(uri)}`;
|
||||||
// Check if user not already in database
|
// Check if user not already in database
|
||||||
const foundUser = await User.fromSql(eq(Users.uri, uri));
|
const foundUser = await User.fromSql(eq(Users.uri, uri.toString()));
|
||||||
|
|
||||||
if (foundUser) {
|
if (foundUser) {
|
||||||
return foundUser;
|
return foundUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if URI is of a local user
|
// Check if URI is of a local user
|
||||||
if (uri.startsWith(config.http.base_url)) {
|
if (uri.origin === config.http.base_url.origin) {
|
||||||
const uuid = uri.match(idValidator);
|
const uuid = uri.href.match(idValidator);
|
||||||
|
|
||||||
if (!uuid?.[0]) {
|
if (!uuid?.[0]) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -893,11 +888,13 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
* @param config The config to use
|
* @param config The config to use
|
||||||
* @returns The raw URL for the user's avatar
|
* @returns The raw URL for the user's avatar
|
||||||
*/
|
*/
|
||||||
public getAvatarUrl(config: Config): string {
|
public getAvatarUrl(config: Config): URL {
|
||||||
if (!this.avatar) {
|
if (!this.avatar) {
|
||||||
return (
|
return (
|
||||||
config.defaults.avatar ||
|
config.defaults.avatar ||
|
||||||
`https://api.dicebear.com/8.x/${config.defaults.placeholder_style}/svg?seed=${this.data.username}`
|
new URL(
|
||||||
|
`https://api.dicebear.com/8.x/${config.defaults.placeholder_style}/svg?seed=${this.data.username}`,
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.avatar?.getUrl();
|
return this.avatar?.getUrl();
|
||||||
|
|
@ -988,9 +985,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
* @param config The config to use
|
* @param config The config to use
|
||||||
* @returns The raw URL for the user's header
|
* @returns The raw URL for the user's header
|
||||||
*/
|
*/
|
||||||
public getHeaderUrl(config: Config): string {
|
public getHeaderUrl(config: Config): URL | null {
|
||||||
if (!this.header) {
|
if (!this.header) {
|
||||||
return config.defaults.header || "";
|
return config.defaults.header ?? null;
|
||||||
}
|
}
|
||||||
return this.header.getUrl();
|
return this.header.getUrl();
|
||||||
}
|
}
|
||||||
|
|
@ -1052,7 +1049,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
*/
|
*/
|
||||||
public async sign(
|
public async sign(
|
||||||
entity: KnownEntity | Collection,
|
entity: KnownEntity | Collection,
|
||||||
signatureUrl: string | URL,
|
signatureUrl: URL,
|
||||||
signatureMethod: HttpVerb = "POST",
|
signatureMethod: HttpVerb = "POST",
|
||||||
): Promise<{
|
): Promise<{
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
|
|
@ -1065,7 +1062,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
|
|
||||||
const output = await signatureConstructor.sign(
|
const output = await signatureConstructor.sign(
|
||||||
signatureMethod,
|
signatureMethod,
|
||||||
new URL(signatureUrl),
|
signatureUrl,
|
||||||
JSON.stringify(entity),
|
JSON.stringify(entity),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1155,7 +1152,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { headers } = await this.sign(entity, inbox);
|
const { headers } = await this.sign(entity, new URL(inbox));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new FederationRequester().post(inbox, entity, {
|
await new FederationRequester().post(inbox, entity, {
|
||||||
|
|
@ -1185,12 +1182,14 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
display_name: user.displayName,
|
display_name: user.displayName,
|
||||||
note: user.note,
|
note: user.note,
|
||||||
uri: this.getUri(),
|
uri: this.getUri().toString(),
|
||||||
url:
|
url:
|
||||||
user.uri ||
|
user.uri ||
|
||||||
new URL(`/@${user.username}`, config.http.base_url).toString(),
|
new URL(`/@${user.username}`, config.http.base_url).toString(),
|
||||||
avatar: proxyUrl(this.getAvatarUrl(config)) ?? "",
|
avatar: proxyUrl(this.getAvatarUrl(config)).toString(),
|
||||||
header: proxyUrl(this.getHeaderUrl(config)) ?? "",
|
header: this.getHeaderUrl(config)
|
||||||
|
? proxyUrl(this.getHeaderUrl(config) as URL).toString()
|
||||||
|
: "",
|
||||||
locked: user.isLocked,
|
locked: user.isLocked,
|
||||||
created_at: new Date(user.createdAt).toISOString(),
|
created_at: new Date(user.createdAt).toISOString(),
|
||||||
followers_count: user.followerCount,
|
followers_count: user.followerCount,
|
||||||
|
|
@ -1204,8 +1203,10 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
bot: user.isBot,
|
bot: user.isBot,
|
||||||
source: isOwnAccount ? user.source : undefined,
|
source: isOwnAccount ? user.source : undefined,
|
||||||
// TODO: Add static avatar and header
|
// TODO: Add static avatar and header
|
||||||
avatar_static: proxyUrl(this.getAvatarUrl(config)) ?? "",
|
avatar_static: proxyUrl(this.getAvatarUrl(config)).toString(),
|
||||||
header_static: proxyUrl(this.getHeaderUrl(config)) ?? "",
|
header_static: this.getHeaderUrl(config)
|
||||||
|
? proxyUrl(this.getHeaderUrl(config) as URL).toString()
|
||||||
|
: "",
|
||||||
acct: this.getAcct(),
|
acct: this.getAcct(),
|
||||||
// TODO: Add these fields
|
// TODO: Add these fields
|
||||||
limited: false,
|
limited: false,
|
||||||
|
|
@ -1258,7 +1259,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
type: "User",
|
type: "User",
|
||||||
uri: this.getUri(),
|
uri: this.getUri().toString(),
|
||||||
bio: {
|
bio: {
|
||||||
"text/html": {
|
"text/html": {
|
||||||
content: user.note,
|
content: user.note,
|
||||||
|
|
@ -1308,11 +1309,12 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
this.getAvatarUrl(config),
|
this.getAvatarUrl(config),
|
||||||
this.data.source.avatar?.content_type,
|
this.data.source.avatar?.content_type,
|
||||||
) ?? undefined,
|
) ?? undefined,
|
||||||
header:
|
header: this.getHeaderUrl(config)
|
||||||
urlToContentFormat(
|
? (urlToContentFormat(
|
||||||
this.getHeaderUrl(config),
|
this.getHeaderUrl(config) as URL,
|
||||||
this.data.source.header?.content_type,
|
this.data.source.header?.content_type,
|
||||||
) ?? undefined,
|
) ?? undefined)
|
||||||
|
: undefined,
|
||||||
display_name: user.displayName,
|
display_name: user.displayName,
|
||||||
fields: user.fields,
|
fields: user.fields,
|
||||||
public_key: {
|
public_key: {
|
||||||
|
|
@ -1335,7 +1337,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
|
|
||||||
public toMention(): ApiMention {
|
public toMention(): ApiMention {
|
||||||
return {
|
return {
|
||||||
url: this.getUri(),
|
url: this.getUri().toString(),
|
||||||
username: this.data.username,
|
username: this.data.username,
|
||||||
acct: this.getAcct(),
|
acct: this.getAcct(),
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ mock.module("~/packages/config-manager/index.ts", () => ({
|
||||||
|
|
||||||
describe("InboxProcessor", () => {
|
describe("InboxProcessor", () => {
|
||||||
let mockRequest: {
|
let mockRequest: {
|
||||||
url: string;
|
url: URL;
|
||||||
method: string;
|
method: string;
|
||||||
body: string;
|
body: string;
|
||||||
};
|
};
|
||||||
|
|
@ -95,7 +95,7 @@ describe("InboxProcessor", () => {
|
||||||
|
|
||||||
// Setup basic mock context
|
// Setup basic mock context
|
||||||
mockRequest = {
|
mockRequest = {
|
||||||
url: "https://test.com",
|
url: new URL("https://test.com"),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: "test-body",
|
body: "test-body",
|
||||||
};
|
};
|
||||||
|
|
@ -196,7 +196,10 @@ describe("InboxProcessor", () => {
|
||||||
|
|
||||||
describe("processNote", () => {
|
describe("processNote", () => {
|
||||||
test("successfully processes valid note", async () => {
|
test("successfully processes valid note", async () => {
|
||||||
const mockNote = { author: "test-author" };
|
const mockNote = {
|
||||||
|
author: "https://example.com",
|
||||||
|
uri: "https://note.example.com",
|
||||||
|
};
|
||||||
const mockAuthor = { id: "test-id" };
|
const mockAuthor = { id: "test-id" };
|
||||||
const mockInstance = { id: "test-id" };
|
const mockInstance = { id: "test-id" };
|
||||||
|
|
||||||
|
|
@ -209,7 +212,9 @@ describe("InboxProcessor", () => {
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
const result = await processor["processNote"]();
|
const result = await processor["processNote"]();
|
||||||
|
|
||||||
expect(User.resolve).toHaveBeenCalledWith("test-author");
|
expect(User.resolve).toHaveBeenCalledWith(
|
||||||
|
new URL("https://example.com"),
|
||||||
|
);
|
||||||
expect(Note.fromVersia).toHaveBeenCalledWith(
|
expect(Note.fromVersia).toHaveBeenCalledWith(
|
||||||
mockNote,
|
mockNote,
|
||||||
mockAuthor,
|
mockAuthor,
|
||||||
|
|
@ -220,7 +225,13 @@ describe("InboxProcessor", () => {
|
||||||
|
|
||||||
test("returns 404 when author not found", async () => {
|
test("returns 404 when author not found", async () => {
|
||||||
User.resolve = jest.fn().mockResolvedValue(null);
|
User.resolve = jest.fn().mockResolvedValue(null);
|
||||||
|
const mockNote = {
|
||||||
|
author: "https://example.com",
|
||||||
|
uri: "https://note.example.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
||||||
|
processor["body"] = mockNote as VersiaNote;
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
const result = await processor["processNote"]();
|
const result = await processor["processNote"]();
|
||||||
|
|
||||||
|
|
@ -233,8 +244,8 @@ describe("InboxProcessor", () => {
|
||||||
describe("processFollowRequest", () => {
|
describe("processFollowRequest", () => {
|
||||||
test("successfully processes follow request for unlocked account", async () => {
|
test("successfully processes follow request for unlocked account", async () => {
|
||||||
const mockFollow = {
|
const mockFollow = {
|
||||||
author: "test-author",
|
author: "https://example.com",
|
||||||
followee: "test-followee",
|
followee: "https://followee.note.com",
|
||||||
};
|
};
|
||||||
const mockAuthor = { id: "author-id" };
|
const mockAuthor = { id: "author-id" };
|
||||||
const mockFollowee = {
|
const mockFollowee = {
|
||||||
|
|
@ -273,7 +284,13 @@ describe("InboxProcessor", () => {
|
||||||
|
|
||||||
test("returns 404 when author not found", async () => {
|
test("returns 404 when author not found", async () => {
|
||||||
User.resolve = jest.fn().mockResolvedValue(null);
|
User.resolve = jest.fn().mockResolvedValue(null);
|
||||||
|
const mockFollow = {
|
||||||
|
author: "https://example.com",
|
||||||
|
followee: "https://followee.note.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
||||||
|
processor["body"] = mockFollow as unknown as Entity;
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
const result = await processor["processFollowRequest"]();
|
const result = await processor["processFollowRequest"]();
|
||||||
|
|
||||||
|
|
@ -287,7 +304,7 @@ describe("InboxProcessor", () => {
|
||||||
test("successfully deletes a note", async () => {
|
test("successfully deletes a note", async () => {
|
||||||
const mockDelete = {
|
const mockDelete = {
|
||||||
deleted_type: "Note",
|
deleted_type: "Note",
|
||||||
deleted: "test-uri",
|
deleted: "https://example.com",
|
||||||
};
|
};
|
||||||
const mockNote = {
|
const mockNote = {
|
||||||
delete: jest.fn(),
|
delete: jest.fn(),
|
||||||
|
|
@ -307,7 +324,7 @@ describe("InboxProcessor", () => {
|
||||||
test("returns 404 when note not found", async () => {
|
test("returns 404 when note not found", async () => {
|
||||||
const mockDelete = {
|
const mockDelete = {
|
||||||
deleted_type: "Note",
|
deleted_type: "Note",
|
||||||
deleted: "test-uri",
|
deleted: "https://example.com",
|
||||||
};
|
};
|
||||||
|
|
||||||
Note.fromSql = jest.fn().mockResolvedValue(null);
|
Note.fromSql = jest.fn().mockResolvedValue(null);
|
||||||
|
|
@ -331,9 +348,9 @@ describe("InboxProcessor", () => {
|
||||||
describe("processLikeRequest", () => {
|
describe("processLikeRequest", () => {
|
||||||
test("successfully processes like request", async () => {
|
test("successfully processes like request", async () => {
|
||||||
const mockLike = {
|
const mockLike = {
|
||||||
author: "test-author",
|
author: "https://example.com",
|
||||||
liked: "test-note",
|
liked: "https://example.note.com",
|
||||||
uri: "test-uri",
|
uri: "https://example.com",
|
||||||
};
|
};
|
||||||
const mockAuthor = {
|
const mockAuthor = {
|
||||||
like: jest.fn(),
|
like: jest.fn(),
|
||||||
|
|
@ -348,13 +365,23 @@ describe("InboxProcessor", () => {
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
const result = await processor["processLikeRequest"]();
|
const result = await processor["processLikeRequest"]();
|
||||||
|
|
||||||
expect(mockAuthor.like).toHaveBeenCalledWith(mockNote, "test-uri");
|
expect(mockAuthor.like).toHaveBeenCalledWith(
|
||||||
|
mockNote,
|
||||||
|
"https://example.com",
|
||||||
|
);
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns 404 when author not found", async () => {
|
test("returns 404 when author not found", async () => {
|
||||||
User.resolve = jest.fn().mockResolvedValue(null);
|
User.resolve = jest.fn().mockResolvedValue(null);
|
||||||
|
const mockLike = {
|
||||||
|
author: "https://example.com",
|
||||||
|
liked: "https://example.note.com",
|
||||||
|
uri: "https://example.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
||||||
|
processor["body"] = mockLike as unknown as Entity;
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
const result = await processor["processLikeRequest"]();
|
const result = await processor["processLikeRequest"]();
|
||||||
|
|
||||||
|
|
@ -367,7 +394,7 @@ describe("InboxProcessor", () => {
|
||||||
describe("processUserRequest", () => {
|
describe("processUserRequest", () => {
|
||||||
test("successfully processes user update", async () => {
|
test("successfully processes user update", async () => {
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
uri: "test-uri",
|
uri: "https://example.com",
|
||||||
};
|
};
|
||||||
const mockUpdatedUser = { id: "user-id" };
|
const mockUpdatedUser = { id: "user-id" };
|
||||||
|
|
||||||
|
|
@ -378,13 +405,20 @@ describe("InboxProcessor", () => {
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
const result = await processor["processUserRequest"]();
|
const result = await processor["processUserRequest"]();
|
||||||
|
|
||||||
expect(User.fetchFromRemote).toHaveBeenCalledWith("test-uri");
|
expect(User.fetchFromRemote).toHaveBeenCalledWith(
|
||||||
|
new URL("https://example.com"),
|
||||||
|
);
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns 500 when update fails", async () => {
|
test("returns 500 when update fails", async () => {
|
||||||
|
const mockUser = {
|
||||||
|
uri: "https://example.com",
|
||||||
|
};
|
||||||
User.fetchFromRemote = jest.fn().mockResolvedValue(null);
|
User.fetchFromRemote = jest.fn().mockResolvedValue(null);
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: Private variable
|
||||||
|
processor["body"] = mockUser as unknown as Entity;
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
const result = await processor["processUserRequest"]();
|
const result = await processor["processUserRequest"]();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ function isDefederated(hostname: string): boolean {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
config.federation.blocked.find(
|
config.federation.blocked.find(
|
||||||
(blocked) => pattern.match(blocked) !== null,
|
(blocked) => pattern.match(blocked.toString()) !== null,
|
||||||
) !== undefined
|
) !== undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +70,7 @@ export class InboxProcessor {
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
private request: {
|
private request: {
|
||||||
url: string;
|
url: URL;
|
||||||
method: string;
|
method: string;
|
||||||
body: string;
|
body: string;
|
||||||
},
|
},
|
||||||
|
|
@ -269,8 +269,8 @@ export class InboxProcessor {
|
||||||
*/
|
*/
|
||||||
private async processNote(): Promise<Response | null> {
|
private async processNote(): Promise<Response | null> {
|
||||||
const note = this.body as VersiaNote;
|
const note = this.body as VersiaNote;
|
||||||
const author = await User.resolve(note.author);
|
const author = await User.resolve(new URL(note.author));
|
||||||
const instance = await Instance.resolve(note.uri);
|
const instance = await Instance.resolve(new URL(note.uri));
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
|
|
@ -298,8 +298,8 @@ export class InboxProcessor {
|
||||||
*/
|
*/
|
||||||
private async processFollowRequest(): Promise<Response | null> {
|
private async processFollowRequest(): Promise<Response | null> {
|
||||||
const follow = this.body as unknown as VersiaFollow;
|
const follow = this.body as unknown as VersiaFollow;
|
||||||
const author = await User.resolve(follow.author);
|
const author = await User.resolve(new URL(follow.author));
|
||||||
const followee = await User.resolve(follow.followee);
|
const followee = await User.resolve(new URL(follow.followee));
|
||||||
|
|
||||||
if (!author) {
|
if (!author) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
|
|
@ -352,8 +352,8 @@ export class InboxProcessor {
|
||||||
*/
|
*/
|
||||||
private async processFollowAccept(): Promise<Response | null> {
|
private async processFollowAccept(): Promise<Response | null> {
|
||||||
const followAccept = this.body as unknown as VersiaFollowAccept;
|
const followAccept = this.body as unknown as VersiaFollowAccept;
|
||||||
const author = await User.resolve(followAccept.author);
|
const author = await User.resolve(new URL(followAccept.author));
|
||||||
const follower = await User.resolve(followAccept.follower);
|
const follower = await User.resolve(new URL(followAccept.follower));
|
||||||
|
|
||||||
if (!author) {
|
if (!author) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
|
|
@ -393,8 +393,8 @@ export class InboxProcessor {
|
||||||
*/
|
*/
|
||||||
private async processFollowReject(): Promise<Response | null> {
|
private async processFollowReject(): Promise<Response | null> {
|
||||||
const followReject = this.body as unknown as VersiaFollowReject;
|
const followReject = this.body as unknown as VersiaFollowReject;
|
||||||
const author = await User.resolve(followReject.author);
|
const author = await User.resolve(new URL(followReject.author));
|
||||||
const follower = await User.resolve(followReject.follower);
|
const follower = await User.resolve(new URL(followReject.follower));
|
||||||
|
|
||||||
if (!author) {
|
if (!author) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
|
|
@ -438,7 +438,7 @@ export class InboxProcessor {
|
||||||
const toDelete = delete_.deleted;
|
const toDelete = delete_.deleted;
|
||||||
|
|
||||||
const author = delete_.author
|
const author = delete_.author
|
||||||
? await User.resolve(delete_.author)
|
? await User.resolve(new URL(delete_.author))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
switch (delete_.deleted_type) {
|
switch (delete_.deleted_type) {
|
||||||
|
|
@ -461,7 +461,7 @@ export class InboxProcessor {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
case "User": {
|
case "User": {
|
||||||
const userToDelete = await User.resolve(toDelete);
|
const userToDelete = await User.resolve(new URL(toDelete));
|
||||||
|
|
||||||
if (!userToDelete) {
|
if (!userToDelete) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
|
|
@ -516,8 +516,8 @@ export class InboxProcessor {
|
||||||
*/
|
*/
|
||||||
private async processLikeRequest(): Promise<Response | null> {
|
private async processLikeRequest(): Promise<Response | null> {
|
||||||
const like = this.body as unknown as VersiaLikeExtension;
|
const like = this.body as unknown as VersiaLikeExtension;
|
||||||
const author = await User.resolve(like.author);
|
const author = await User.resolve(new URL(like.author));
|
||||||
const likedNote = await Note.resolve(like.liked);
|
const likedNote = await Note.resolve(new URL(like.liked));
|
||||||
|
|
||||||
if (!author) {
|
if (!author) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
|
|
@ -546,7 +546,7 @@ export class InboxProcessor {
|
||||||
private async processUserRequest(): Promise<Response | null> {
|
private async processUserRequest(): Promise<Response | null> {
|
||||||
const user = this.body as unknown as VersiaUser;
|
const user = this.body as unknown as VersiaUser;
|
||||||
// FIXME: Instead of refetching the remote user, we should read the incoming json and update from that
|
// FIXME: Instead of refetching the remote user, we should read the incoming json and update from that
|
||||||
const updatedAccount = await User.fetchFromRemote(user.uri);
|
const updatedAccount = await User.fetchFromRemote(new URL(user.uri));
|
||||||
|
|
||||||
if (!updatedAccount) {
|
if (!updatedAccount) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export const getFetchWorker = (): Worker<FetchJobData, void, FetchJobType> =>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Instance.resolve(uri);
|
await Instance.resolve(new URL(uri));
|
||||||
|
|
||||||
await job.log(
|
await job.log(
|
||||||
`✔ Finished fetching instance metadata from [${uri}]`,
|
`✔ Finished fetching instance metadata from [${uri}]`,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,10 @@ export const getInboxWorker = (): Worker<InboxJobData, void, InboxJobType> =>
|
||||||
|
|
||||||
if (headers.authorization) {
|
if (headers.authorization) {
|
||||||
const processor = new InboxProcessor(
|
const processor = new InboxProcessor(
|
||||||
request,
|
{
|
||||||
|
...request,
|
||||||
|
url: new URL(request.url),
|
||||||
|
},
|
||||||
data,
|
data,
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
|
|
@ -67,7 +70,7 @@ export const getInboxWorker = (): Worker<InboxJobData, void, InboxJobType> =>
|
||||||
"x-signed-by": string;
|
"x-signed-by": string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sender = await User.resolve(signedBy);
|
const sender = await User.resolve(new URL(signedBy));
|
||||||
|
|
||||||
if (!(sender || signedBy.startsWith("instance "))) {
|
if (!(sender || signedBy.startsWith("instance "))) {
|
||||||
await job.log(
|
await job.log(
|
||||||
|
|
@ -106,7 +109,10 @@ export const getInboxWorker = (): Worker<InboxJobData, void, InboxJobType> =>
|
||||||
}
|
}
|
||||||
|
|
||||||
const processor = new InboxProcessor(
|
const processor = new InboxProcessor(
|
||||||
request,
|
{
|
||||||
|
...request,
|
||||||
|
url: new URL(request.url),
|
||||||
|
},
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
instance: remoteInstance,
|
instance: remoteInstance,
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ export const getPushWorker = (): Worker<PushJobData, void, PushJobType> =>
|
||||||
vapidDetails: {
|
vapidDetails: {
|
||||||
subject:
|
subject:
|
||||||
config.notifications.push.vapid.subject ||
|
config.notifications.push.vapid.subject ||
|
||||||
config.http.base_url,
|
config.http.base_url.origin,
|
||||||
privateKey: config.notifications.push.vapid.private,
|
privateKey: config.notifications.push.vapid.private,
|
||||||
publicKey: config.notifications.push.vapid.public,
|
publicKey: config.notifications.push.vapid.public,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ export abstract class UserFinderCommand<
|
||||||
|
|
||||||
// Check instance exists, if not, create it
|
// Check instance exists, if not, create it
|
||||||
await Instance.resolve(
|
await Instance.resolve(
|
||||||
`https://${parseUserAddress(identifier).domain}`,
|
new URL(`https://${parseUserAddress(identifier).domain}`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export default class FederationUserFetch extends BaseCommand<
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check instance exists, if not, create it
|
// Check instance exists, if not, create it
|
||||||
await Instance.resolve(`https://${host}`);
|
await Instance.resolve(new URL(`https://${host}`));
|
||||||
|
|
||||||
const manager = await User.getFederationRequester();
|
const manager = await User.getFederationRequester();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export default class FederationUserFinger extends BaseCommand<
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check instance exists, if not, create it
|
// Check instance exists, if not, create it
|
||||||
await Instance.resolve(`https://${host}`);
|
await Instance.resolve(new URL(`https://${host}`));
|
||||||
|
|
||||||
const manager = await User.getFederationRequester();
|
const manager = await User.getFederationRequester();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ const zUrl = z
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
.refine((arg) => URL.canParse(arg), "Invalid url")
|
.refine((arg) => URL.canParse(arg), "Invalid url")
|
||||||
.transform((arg) => arg.replace(/\/$/, ""));
|
.transform((arg) => arg.replace(/\/$/, ""))
|
||||||
|
.transform((arg) => new URL(arg));
|
||||||
|
|
||||||
export const configValidator = z
|
export const configValidator = z
|
||||||
.object({
|
.object({
|
||||||
|
|
@ -124,7 +125,7 @@ export const configValidator = z
|
||||||
.strict(),
|
.strict(),
|
||||||
http: z
|
http: z
|
||||||
.object({
|
.object({
|
||||||
base_url: z.string().min(1).default("http://versia.social"),
|
base_url: zUrl.default("http://versia.social"),
|
||||||
bind: z.string().min(1).default("0.0.0.0"),
|
bind: z.string().min(1).default("0.0.0.0"),
|
||||||
bind_port: z
|
bind_port: z
|
||||||
.number()
|
.number()
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,9 @@ export default (plugin: PluginType): void => {
|
||||||
{
|
{
|
||||||
id: issuer.id,
|
id: issuer.id,
|
||||||
name: issuer.name,
|
name: issuer.name,
|
||||||
icon: proxyUrl(issuer.icon) ?? undefined,
|
icon: issuer.icon
|
||||||
|
? proxyUrl(new URL(issuer.icon))
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,9 @@ export default (plugin: PluginType): void => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const authServer = await oauthDiscoveryRequest(issuer.url);
|
const authServer = await oauthDiscoveryRequest(
|
||||||
|
new URL(issuer.url),
|
||||||
|
);
|
||||||
|
|
||||||
const codeVerifier = generateRandomCodeVerifier();
|
const codeVerifier = generateRandomCodeVerifier();
|
||||||
|
|
||||||
|
|
@ -130,7 +132,7 @@ export default (plugin: PluginType): void => {
|
||||||
crypto.getRandomValues(new Uint8Array(32)),
|
crypto.getRandomValues(new Uint8Array(32)),
|
||||||
).toString("base64"),
|
).toString("base64"),
|
||||||
name: "Versia",
|
name: "Versia",
|
||||||
redirectUri,
|
redirectUri: redirectUri.toString(),
|
||||||
scopes: "openid profile email",
|
scopes: "openid profile email",
|
||||||
secret: "",
|
secret: "",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,15 @@ import {
|
||||||
} from "oauth4webapi";
|
} from "oauth4webapi";
|
||||||
|
|
||||||
export const oauthDiscoveryRequest = (
|
export const oauthDiscoveryRequest = (
|
||||||
issuerUrl: string | URL,
|
issuerUrl: URL,
|
||||||
): Promise<AuthorizationServer> => {
|
): Promise<AuthorizationServer> => {
|
||||||
const issuerUrlurl = new URL(issuerUrl);
|
return discoveryRequest(issuerUrl, {
|
||||||
|
|
||||||
return discoveryRequest(issuerUrlurl, {
|
|
||||||
algorithm: "oidc",
|
algorithm: "oidc",
|
||||||
}).then((res) => processDiscoveryResponse(issuerUrlurl, res));
|
}).then((res) => processDiscoveryResponse(issuerUrl, res));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const oauthRedirectUri = (baseUrl: string, issuer: string): string =>
|
export const oauthRedirectUri = (baseUrl: URL, issuer: string): URL =>
|
||||||
new URL(`/oauth/sso/${issuer}/callback`, baseUrl).toString();
|
new URL(`/oauth/sso/${issuer}/callback`, baseUrl);
|
||||||
|
|
||||||
const getFlow = (
|
const getFlow = (
|
||||||
flowId: string,
|
flowId: string,
|
||||||
|
|
@ -73,7 +71,7 @@ const getOIDCResponse = (
|
||||||
authServer: AuthorizationServer,
|
authServer: AuthorizationServer,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
clientSecret: string,
|
clientSecret: string,
|
||||||
redirectUri: string,
|
redirectUri: URL,
|
||||||
codeVerifier: string,
|
codeVerifier: string,
|
||||||
parameters: URLSearchParams,
|
parameters: URLSearchParams,
|
||||||
): Promise<Response> => {
|
): Promise<Response> => {
|
||||||
|
|
@ -84,7 +82,7 @@ const getOIDCResponse = (
|
||||||
},
|
},
|
||||||
ClientSecretPost(clientSecret),
|
ClientSecretPost(clientSecret),
|
||||||
parameters,
|
parameters,
|
||||||
redirectUri,
|
redirectUri.toString(),
|
||||||
codeVerifier,
|
codeVerifier,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -177,7 +175,7 @@ export const automaticOidcFlow = async (
|
||||||
authServer,
|
authServer,
|
||||||
issuer.client_id,
|
issuer.client_id,
|
||||||
issuer.client_secret,
|
issuer.client_secret,
|
||||||
redirectUrl.toString(),
|
redirectUrl,
|
||||||
flow.codeVerifier,
|
flow.codeVerifier,
|
||||||
parameters,
|
parameters,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export const applyToHono = (app: OpenAPIHono<HonoEnv>): void => {
|
||||||
},
|
},
|
||||||
boardLogo: {
|
boardLogo: {
|
||||||
path:
|
path:
|
||||||
config.instance.logo ??
|
config.instance.logo?.toString() ??
|
||||||
"https://cdn.versia.pub/branding/icon.svg",
|
"https://cdn.versia.pub/branding/icon.svg",
|
||||||
height: 40,
|
height: 40,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/packages/config-manager/index.ts";
|
||||||
|
|
||||||
export const localObjectUri = (id: string): string =>
|
export const localObjectUri = (id: string): URL =>
|
||||||
new URL(`/objects/${id}`, config.http.base_url).toString();
|
new URL(`/objects/${id}`, config.http.base_url);
|
||||||
|
|
|
||||||
|
|
@ -30,28 +30,25 @@ export const getBestContentType = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const urlToContentFormat = (
|
export const urlToContentFormat = (
|
||||||
url: string,
|
url: URL,
|
||||||
contentType?: string,
|
contentType?: string,
|
||||||
): ContentFormat | null => {
|
): ContentFormat | null => {
|
||||||
if (!url) {
|
if (url.href.startsWith("https://api.dicebear.com/")) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (url.startsWith("https://api.dicebear.com/")) {
|
|
||||||
return {
|
return {
|
||||||
"image/svg+xml": {
|
"image/svg+xml": {
|
||||||
content: url,
|
content: url.toString(),
|
||||||
remote: true,
|
remote: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const mimeType =
|
const mimeType =
|
||||||
contentType ||
|
contentType ||
|
||||||
lookup(url.replace(new URL(url).search, "")) ||
|
lookup(url.toString().replace(url.search, "")) ||
|
||||||
"application/octet-stream";
|
"application/octet-stream";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[mimeType]: {
|
[mimeType]: {
|
||||||
content: url,
|
content: url.toString(),
|
||||||
remote: true,
|
remote: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,9 @@ export type Json =
|
||||||
| Json[]
|
| Json[]
|
||||||
| { [key: string]: Json };
|
| { [key: string]: Json };
|
||||||
|
|
||||||
export const proxyUrl = (url: string | null = null): string | null => {
|
export const proxyUrl = (url: URL): URL => {
|
||||||
const urlAsBase64Url = Buffer.from(url || "").toString("base64url");
|
const urlAsBase64Url = Buffer.from(url.toString() || "").toString(
|
||||||
return url
|
"base64url",
|
||||||
? new URL(
|
);
|
||||||
`/media/proxy/${urlAsBase64Url}`,
|
return new URL(`/media/proxy/${urlAsBase64Url}`, config.http.base_url);
|
||||||
config.http.base_url,
|
|
||||||
).toString()
|
|
||||||
: url;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,11 @@ export const sanitizeHtml = async (
|
||||||
element(element): void {
|
element(element): void {
|
||||||
element.setAttribute(
|
element.setAttribute(
|
||||||
"src",
|
"src",
|
||||||
proxyUrl(element.getAttribute("src") ?? "") ?? "",
|
element.getAttribute("src")
|
||||||
|
? proxyUrl(
|
||||||
|
new URL(element.getAttribute("src") as string),
|
||||||
|
).toString()
|
||||||
|
: "",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue