diff --git a/api/notes/[uuid]/quotes.ts b/api/notes/[uuid]/quotes.ts index 2293f737..3dd1a996 100644 --- a/api/notes/[uuid]/quotes.ts +++ b/api/notes/[uuid]/quotes.ts @@ -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 diff --git a/api/notes/[uuid]/replies.ts b/api/notes/[uuid]/replies.ts index ffddf263..49465e5a 100644 --- a/api/notes/[uuid]/replies.ts +++ b/api/notes/[uuid]/replies.ts @@ -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 diff --git a/api/users/[uuid]/outbox/index.ts b/api/users/[uuid]/outbox/index.ts index 67a610e0..c4545ee7 100644 --- a/api/users/[uuid]/outbox/index.ts +++ b/api/users/[uuid]/outbox/index.ts @@ -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()), }); diff --git a/classes/database/instance.ts b/classes/database/instance.ts index 44ff4938..7033b5e7 100644 --- a/classes/database/instance.ts +++ b/classes/database/instance.ts @@ -306,7 +306,7 @@ export class Instance extends BaseInterface { 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 { 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, }); diff --git a/classes/database/like.ts b/classes/database/like.ts index b4797941..f7dbbf1d 100644 --- a/classes/database/like.ts +++ b/classes/database/like.ts @@ -155,13 +155,14 @@ export class Like extends BaseInterface { 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 { : 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, }); } } diff --git a/classes/database/note.ts b/classes/database/note.ts index ad6662b2..b3b91f7f 100644 --- a/classes/database/note.ts +++ b/classes/database/note.ts @@ -449,14 +449,14 @@ export class Note extends BaseInterface { 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 { 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 { 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 { 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 { 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 { 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 { >, ), 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 diff --git a/classes/database/reaction.ts b/classes/database/reaction.ts index 85092dd9..4c80bda4 100644 --- a/classes/database/reaction.ts +++ b/classes/database/reaction.ts @@ -179,17 +179,18 @@ export class Reaction extends BaseInterface { } 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 { 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, diff --git a/classes/database/user.ts b/classes/database/user.ts index 50d6c037..84b4be2a 100644 --- a/classes/database/user.ts +++ b/classes/database/user.ts @@ -247,9 +247,9 @@ export class User extends BaseInterface { 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 { 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 { 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 { 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 { username, id: randomUUIDv7(), publicKey: public_key.key, - uri: uri.href, + uri, instanceId: instance.id, })); @@ -727,13 +727,13 @@ export class User extends BaseInterface { 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 { 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 { 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 { 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", }, diff --git a/classes/inbox/processor.ts b/classes/inbox/processor.ts index 3cbdc899..750c5678 100644 --- a/classes/inbox/processor.ts +++ b/classes/inbox/processor.ts @@ -219,8 +219,8 @@ export class InboxProcessor { private static async processFollowRequest( follow: VersiaEntities.Follow, ): Promise { - 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 { - 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 { - 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 { - 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)); } /** diff --git a/classes/queues/push.ts b/classes/queues/push.ts index e9e5d9f4..a92c18c8 100644 --- a/classes/queues/push.ts +++ b/classes/queues/push.ts @@ -35,6 +35,16 @@ export const getPushWorker = (): Worker => 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 => 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", }, diff --git a/packages/sdk/http.ts b/packages/sdk/http.ts index 86e8a43b..7a6a6e72 100644 --- a/packages/sdk/http.ts +++ b/packages/sdk/http.ts @@ -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 { - 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)); } /** diff --git a/packages/sdk/schemas/common.ts b/packages/sdk/schemas/common.ts index 468059ef..63cc13ba 100644 --- a/packages/sdk/schemas/common.ts +++ b/packages/sdk/schemas/common.ts @@ -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();