refactor(api): ♻️ Use URL literal instead of strings

This commit is contained in:
Jesse Wierzbinski 2025-02-01 16:32:18 +01:00
parent 99fac323c8
commit 76d1ccc859
No known key found for this signature in database
50 changed files with 343 additions and 256 deletions

View file

@ -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,

View file

@ -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) {

View file

@ -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(
this.data.liked.id,
this.data.liked.uri,
) as string,
liked:
Note.getUri(
this.data.liked.id,
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(),
};

View file

@ -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,11 +220,16 @@ 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, {
description: options?.description,
})
: undefined;
const thumbnailContent =
thumbnailUrl && options?.thumbnail
? await Media.fileToContentFormat(
options.thumbnail,
thumbnailUrl,
{
description: options?.description,
},
)
: undefined;
const newAttachment = await Media.insert({
content,
@ -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 new URL(`/${name}`, config.s3?.public_url);
}
return "";
throw new Error("Unknown media backend");
}
public getUrl(): string {
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,

View file

@ -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",

View file

@ -179,14 +179,13 @@ export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
};
}
public getUri(baseUrl: string): string {
return (
this.data.uri ||
new URL(
`/objects/${this.data.noteId}/reactions/${this.id}`,
baseUrl,
).toString()
);
public getUri(baseUrl: URL): URL {
return this.data.uri
? new URL(this.data.uri)
: new URL(
`/objects/${this.data.noteId}/reactions/${this.id}`,
baseUrl,
);
}
public isLocal(): boolean {
@ -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(
this.data.note.id,
this.data.note.uri,
) as string,
object:
Note.getUri(
this.data.note.id,
this.data.note.uri
? new URL(this.data.note.uri)
: undefined,
)?.toString() ?? "",
content: this.hasCustomEmoji()
? `:${this.data.emoji?.shortcode}:`
: this.data.emojiText || "",

View file

@ -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,
};
}
}

View file

@ -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);

View file

@ -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(
username,
hostname,
"application/activity+json",
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),
this.data.source.header?.content_type,
) ?? undefined,
header: this.getHeaderUrl(config)
? (urlToContentFormat(
this.getHeaderUrl(config) as URL,
this.data.source.header?.content_type,
) ?? 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,