fix(api): 🐛 Don't use URL in Versia entity schemas, fixes OpenAPI

This commit is contained in:
Jesse Wierzbinski 2025-04-16 16:35:17 +02:00
parent 0a712128a5
commit a2e907390f
No known key found for this signature in database
12 changed files with 131 additions and 102 deletions

View file

@ -86,11 +86,11 @@ export default apiRoute((app) =>
);
const uriCollection = new VersiaEntities.URICollection({
author: note.author.uri,
author: note.author.uri.href,
first: new URL(
`/notes/${note.id}/quotes?offset=0`,
config.http.base_url,
),
).href,
last:
replyCount > limit
? new URL(
@ -98,11 +98,11 @@ export default apiRoute((app) =>
replyCount - limit
}`,
config.http.base_url,
)
).href
: new URL(
`/notes/${note.id}/quotes`,
config.http.base_url,
),
).href,
next:
offset + limit < replyCount
? new URL(
@ -110,7 +110,7 @@ export default apiRoute((app) =>
offset + limit
}`,
config.http.base_url,
)
).href
: null,
previous:
offset - limit >= 0
@ -119,10 +119,10 @@ export default apiRoute((app) =>
offset - limit
}`,
config.http.base_url,
)
).href
: null,
total: replyCount,
items: replies.map((reply) => reply.getUri()),
items: replies.map((reply) => reply.getUri().href),
});
// If base_url uses https and request uses http, rewrite request to use https

View file

@ -84,11 +84,11 @@ export default apiRoute((app) =>
);
const uriCollection = new VersiaEntities.URICollection({
author: note.author.uri,
author: note.author.uri.href,
first: new URL(
`/notes/${note.id}/replies?offset=0`,
config.http.base_url,
),
).href,
last:
replyCount > limit
? new URL(
@ -96,11 +96,11 @@ export default apiRoute((app) =>
replyCount - limit
}`,
config.http.base_url,
)
).href
: new URL(
`/notes/${note.id}/replies`,
config.http.base_url,
),
).href,
next:
offset + limit < replyCount
? new URL(
@ -108,7 +108,7 @@ export default apiRoute((app) =>
offset + limit
}`,
config.http.base_url,
)
).href
: null,
previous:
offset - limit >= 0
@ -117,10 +117,10 @@ export default apiRoute((app) =>
offset - limit
}`,
config.http.base_url,
)
).href
: null,
total: replyCount,
items: replies.map((reply) => reply.getUri()),
items: replies.map((reply) => reply.getUri().href),
});
// If base_url uses https and request uses http, rewrite request to use https

View file

@ -98,28 +98,28 @@ export default apiRoute((app) =>
first: new URL(
`/users/${uuid}/outbox?page=1`,
config.http.base_url,
),
).href,
last: new URL(
`/users/${uuid}/outbox?page=${Math.ceil(
totalNotes / NOTES_PER_PAGE,
)}`,
config.http.base_url,
),
).href,
total: totalNotes,
author: author.uri,
author: author.uri.href,
next:
notes.length === NOTES_PER_PAGE
? new URL(
`/users/${uuid}/outbox?page=${pageNumber + 1}`,
config.http.base_url,
)
).href
: null,
previous:
pageNumber > 1
? new URL(
`/users/${uuid}/outbox?page=${pageNumber - 1}`,
config.http.base_url,
)
).href
: null,
items: notes.map((note) => note.toVersia()),
});

View file

@ -306,7 +306,7 @@ export class Instance extends BaseInterface<typeof Instances> {
logo: metadata.data.logo,
protocol,
publicKey: metadata.data.public_key,
inbox: metadata.data.shared_inbox?.href ?? null,
inbox: metadata.data.shared_inbox ?? null,
extensions: metadata.data.extensions ?? null,
});
}
@ -333,7 +333,7 @@ export class Instance extends BaseInterface<typeof Instances> {
logo: metadata.data.logo,
protocol,
publicKey: metadata.data.public_key,
inbox: metadata.data.shared_inbox?.href ?? null,
inbox: metadata.data.shared_inbox ?? null,
extensions: metadata.data.extensions ?? null,
});

View file

@ -155,13 +155,14 @@ export class Like extends BaseInterface<typeof Likes, LikeType> {
author: User.getUri(
this.data.liker.id,
this.data.liker.uri ? new URL(this.data.liker.uri) : null,
),
).href,
type: "pub.versia:likes/Like",
created_at: new Date(this.data.createdAt).toISOString(),
liked: this.data.liked.uri
? new URL(this.data.liked.uri)
: new URL(`/notes/${this.data.liked.id}`, config.http.base_url),
uri: this.getUri(),
? new URL(this.data.liked.uri).href
: new URL(`/notes/${this.data.liked.id}`, config.http.base_url)
.href,
uri: this.getUri().href,
});
}
@ -177,9 +178,9 @@ export class Like extends BaseInterface<typeof Likes, LikeType> {
: this.data.liker.uri
? new URL(this.data.liker.uri)
: null,
),
).href,
deleted_type: "pub.versia:likes/Like",
deleted: this.getUri(),
deleted: this.getUri().href,
});
}
}

View file

@ -449,14 +449,14 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
replies_to,
subject,
} = versiaNote.data;
const instance = await Instance.resolve(authorUrl);
const author = await User.resolve(authorUrl);
const instance = await Instance.resolve(new URL(authorUrl));
const author = await User.resolve(new URL(authorUrl));
if (!author) {
throw new Error("Entity author could not be resolved");
}
const existingNote = await Note.fromSql(eq(Notes.uri, uri.href));
const existingNote = await Note.fromSql(eq(Notes.uri, uri));
const note =
existingNote ??
@ -464,7 +464,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
id: randomUUIDv7(),
authorId: author.id,
visibility: "public",
uri: uri.href,
uri,
createdAt: new Date(created_at).toISOString(),
}));
@ -480,15 +480,22 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
const mentions = (
await Promise.all(
noteMentions?.map((mention) => User.resolve(mention)) ?? [],
noteMentions?.map((mention) =>
User.resolve(new URL(mention)),
) ?? [],
)
).filter((m) => m !== null);
// TODO: Implement groups
const visibility = !group || group instanceof URL ? "direct" : group;
const visibility =
!group || URL.canParse(group)
? "direct"
: (group as "public" | "followers" | "unlisted");
const reply = replies_to ? await Note.resolve(replies_to) : null;
const quote = quotes ? await Note.resolve(quotes) : null;
const reply = replies_to
? await Note.resolve(new URL(replies_to))
: null;
const quote = quotes ? await Note.resolve(new URL(quotes)) : null;
const spoiler = subject ? await sanitizedHtmlStrip(subject) : undefined;
await note.update({
@ -694,9 +701,9 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
return new VersiaEntities.Delete({
type: "Delete",
id,
author: this.author.uri,
author: this.author.uri.href,
deleted_type: "Note",
deleted: this.getUri(),
deleted: this.getUri().href,
created_at: new Date().toISOString(),
});
}
@ -711,8 +718,8 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
type: "Note",
created_at: new Date(status.createdAt).toISOString(),
id: status.id,
author: this.author.uri,
uri: this.getUri(),
author: this.author.uri.href,
uri: this.getUri().href,
content: {
"text/html": {
content: status.content,
@ -727,11 +734,11 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
replies: new URL(
`/notes/${status.id}/replies`,
config.http.base_url,
),
).href,
quotes: new URL(
`/notes/${status.id}/quotes`,
config.http.base_url,
),
).href,
},
attachments: status.attachments.map(
(attachment) =>
@ -740,21 +747,24 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
>,
),
is_sensitive: status.sensitive,
mentions: status.mentions.map((mention) =>
User.getUri(
mention.id,
mention.uri ? new URL(mention.uri) : null,
),
mentions: status.mentions.map(
(mention) =>
User.getUri(
mention.id,
mention.uri ? new URL(mention.uri) : null,
).href,
),
quotes: status.quote
? status.quote.uri
? new URL(status.quote.uri)
? new URL(status.quote.uri).href
: new URL(`/notes/${status.quote.id}`, config.http.base_url)
.href
: null,
replies_to: status.reply
? status.reply.uri
? new URL(status.reply.uri)
? new URL(status.reply.uri).href
: new URL(`/notes/${status.reply.id}`, config.http.base_url)
.href
: null,
subject: status.spoilerText,
// TODO: Refactor as part of groups

View file

@ -179,17 +179,18 @@ export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
}
return new VersiaEntities.Reaction({
uri: this.getUri(config.http.base_url),
uri: this.getUri(config.http.base_url).href,
type: "pub.versia:reactions/Reaction",
author: User.getUri(
this.data.authorId,
this.data.author.uri ? new URL(this.data.author.uri) : null,
),
).href,
created_at: new Date(this.data.createdAt).toISOString(),
id: this.id,
object: this.data.note.uri
? new URL(this.data.note.uri)
: new URL(`/notes/${this.data.noteId}`, config.http.base_url),
? new URL(this.data.note.uri).href
: new URL(`/notes/${this.data.noteId}`, config.http.base_url)
.href,
content: this.hasCustomEmoji()
? `:${this.data.emoji?.shortcode}:`
: this.data.emojiText || "",
@ -232,7 +233,7 @@ export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
return Reaction.insert({
id: randomUUIDv7(),
uri: reactionToConvert.data.uri.href,
uri: reactionToConvert.data.uri,
authorId: author.id,
noteId: note.id,
emojiId: emoji ? emoji.id : null,

View file

@ -247,9 +247,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
return new VersiaEntities.Unfollow({
type: "Unfollow",
id,
author: this.uri,
author: this.uri.href,
created_at: new Date().toISOString(),
followee: followee.uri,
followee: followee.uri.href,
});
}
@ -265,9 +265,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
const entity = new VersiaEntities.FollowAccept({
type: "FollowAccept",
id: crypto.randomUUID(),
author: this.uri,
author: this.uri.href,
created_at: new Date().toISOString(),
follower: follower.uri,
follower: follower.uri.href,
});
await deliveryQueue.add(DeliveryJobType.FederateEntity, {
@ -289,9 +289,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
const entity = new VersiaEntities.FollowReject({
type: "FollowReject",
id: crypto.randomUUID(),
author: this.uri,
author: this.uri.href,
created_at: new Date().toISOString(),
follower: follower.uri,
follower: follower.uri.href,
});
await deliveryQueue.add(DeliveryJobType.FederateEntity, {
@ -675,9 +675,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
uri,
extensions,
} = versiaUser.data;
const instance = await Instance.resolve(versiaUser.data.uri);
const instance = await Instance.resolve(new URL(versiaUser.data.uri));
const existingUser = await User.fromSql(
eq(Users.uri, versiaUser.data.uri.href),
eq(Users.uri, versiaUser.data.uri),
);
const user =
@ -686,7 +686,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
username,
id: randomUUIDv7(),
publicKey: public_key.key,
uri: uri.href,
uri,
instanceId: instance.id,
}));
@ -727,13 +727,13 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
await user.update({
createdAt: new Date(created_at).toISOString(),
endpoints: {
inbox: inbox.href,
outbox: collections.outbox.href,
followers: collections.followers.href,
following: collections.following.href,
featured: collections.featured.href,
likes: collections["pub.versia:likes/Likes"]?.href,
dislikes: collections["pub.versia:likes/Dislikes"]?.href,
inbox,
outbox: collections.outbox,
followers: collections.followers,
following: collections.following,
featured: collections.featured,
likes: collections["pub.versia:likes/Likes"] ?? undefined,
dislikes: collections["pub.versia:likes/Dislikes"] ?? undefined,
},
avatarId: userAvatar?.id,
headerId: userHeader?.id,
@ -1097,7 +1097,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
return new VersiaEntities.User({
id: user.id,
type: "User",
uri: this.uri,
uri: this.uri.href,
bio: {
"text/html": {
content: user.note,
@ -1113,29 +1113,30 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
featured: new URL(
`/users/${user.id}/featured`,
config.http.base_url,
),
).href,
"pub.versia:likes/Likes": new URL(
`/users/${user.id}/likes`,
config.http.base_url,
),
).href,
"pub.versia:likes/Dislikes": new URL(
`/users/${user.id}/dislikes`,
config.http.base_url,
),
).href,
followers: new URL(
`/users/${user.id}/followers`,
config.http.base_url,
),
).href,
following: new URL(
`/users/${user.id}/following`,
config.http.base_url,
),
).href,
outbox: new URL(
`/users/${user.id}/outbox`,
config.http.base_url,
),
).href,
},
inbox: new URL(`/users/${user.id}/inbox`, config.http.base_url),
inbox: new URL(`/users/${user.id}/inbox`, config.http.base_url)
.href,
indexable: this.data.isIndexable,
username: user.username,
manually_approves_followers: this.data.isLocked,
@ -1148,7 +1149,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
display_name: user.displayName,
fields: user.fields,
public_key: {
actor: new URL(`/users/${user.id}`, config.http.base_url),
actor: new URL(`/users/${user.id}`, config.http.base_url).href,
key: user.publicKey,
algorithm: "ed25519",
},

View file

@ -219,8 +219,8 @@ export class InboxProcessor {
private static async processFollowRequest(
follow: VersiaEntities.Follow,
): Promise<void> {
const author = await User.resolve(follow.data.author);
const followee = await User.resolve(follow.data.followee);
const author = await User.resolve(new URL(follow.data.author));
const followee = await User.resolve(new URL(follow.data.followee));
if (!author) {
throw new ApiError(404, "Author not found");
@ -267,8 +267,10 @@ export class InboxProcessor {
private static async processFollowAccept(
followAccept: VersiaEntities.FollowAccept,
): Promise<void> {
const author = await User.resolve(followAccept.data.author);
const follower = await User.resolve(followAccept.data.follower);
const author = await User.resolve(new URL(followAccept.data.author));
const follower = await User.resolve(
new URL(followAccept.data.follower),
);
if (!author) {
throw new ApiError(404, "Author not found");
@ -302,8 +304,10 @@ export class InboxProcessor {
private static async processFollowReject(
followReject: VersiaEntities.FollowReject,
): Promise<void> {
const author = await User.resolve(followReject.data.author);
const follower = await User.resolve(followReject.data.follower);
const author = await User.resolve(new URL(followReject.data.author));
const follower = await User.resolve(
new URL(followReject.data.follower),
);
if (!author) {
throw new ApiError(404, "Author not found");
@ -340,13 +344,13 @@ export class InboxProcessor {
const toDelete = delete_.data.deleted;
const author = delete_.data.author
? await User.resolve(delete_.data.author)
? await User.resolve(new URL(delete_.data.author))
: null;
switch (delete_.data.deleted_type) {
case "Note": {
const note = await Note.fromSql(
eq(Notes.uri, toDelete.href),
eq(Notes.uri, toDelete),
author ? eq(Notes.authorId, author.id) : undefined,
);
@ -361,7 +365,7 @@ export class InboxProcessor {
return;
}
case "User": {
const userToDelete = await User.resolve(toDelete);
const userToDelete = await User.resolve(new URL(toDelete));
if (!userToDelete) {
throw new ApiError(404, "User to delete not found");
@ -376,7 +380,7 @@ export class InboxProcessor {
}
case "pub.versia:likes/Like": {
const like = await Like.fromSql(
eq(Likes.uri, toDelete.href),
eq(Likes.uri, toDelete),
author ? eq(Likes.likerId, author.id) : undefined,
);
@ -393,7 +397,7 @@ export class InboxProcessor {
default: {
throw new ApiError(
400,
`Deletion of object ${toDelete.href} not implemented`,
`Deletion of object ${toDelete} not implemented`,
);
}
}
@ -408,8 +412,8 @@ export class InboxProcessor {
private static async processLikeRequest(
like: VersiaEntities.Like,
): Promise<void> {
const author = await User.resolve(like.data.author);
const likedNote = await Note.resolve(like.data.liked);
const author = await User.resolve(new URL(like.data.author));
const likedNote = await Note.resolve(new URL(like.data.liked));
if (!author) {
throw new ApiError(404, "Author not found");
@ -419,7 +423,7 @@ export class InboxProcessor {
throw new ApiError(404, "Liked Note not found");
}
await author.like(likedNote, like.data.uri);
await author.like(likedNote, new URL(like.data.uri));
}
/**

View file

@ -35,6 +35,16 @@ export const getPushWorker = (): Worker<PushJobData, void, PushJobType> =>
return;
}
if (
!(
config.notifications.push.vapid_keys.private ||
config.notifications.push.vapid_keys.public
)
) {
await job.log("Push notifications are not configured");
return;
}
await job.log(
`Sending push notification for note [${notificationId}]`,
);
@ -132,8 +142,9 @@ export const getPushWorker = (): Worker<PushJobData, void, PushJobType> =>
config.notifications.push.subject ||
config.http.base_url.origin,
privateKey:
config.notifications.push.vapid_keys.private,
publicKey: config.notifications.push.vapid_keys.public,
config.notifications.push.vapid_keys.private ?? "",
publicKey:
config.notifications.push.vapid_keys.public ?? "",
},
contentEncoding: "aesgcm",
},

View file

@ -118,7 +118,9 @@ export class FederationRequester {
}
}
nextUrl = collection.data.next;
nextUrl = collection.data.next
? new URL(collection.data.next)
: null;
limit -= collection.data.items.length;
}
@ -136,7 +138,7 @@ export class FederationRequester {
limit?: number;
},
): Promise<URL[]> {
const entities: URL[] = [];
const entities: string[] = [];
let nextUrl: URL | null = url;
let limit = options?.limit ?? Number.POSITIVE_INFINITY;
@ -147,11 +149,13 @@ export class FederationRequester {
);
entities.push(...collection.data.items);
nextUrl = collection.data.next;
nextUrl = collection.data.next
? new URL(collection.data.next)
: null;
limit -= collection.data.items.length;
}
return entities;
return entities.map((u) => new URL(u));
}
/**

View file

@ -11,7 +11,4 @@ export const u64 = z
.nonnegative()
.max(2 ** 64 - 1);
export const url = z
.string()
.url()
.transform((z) => new URL(z));
export const url = z.string().url();