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