From 5114df44547897070995b97de68084ee17067a42 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 13 Feb 2025 18:03:04 +0100 Subject: [PATCH] feat(federation): :alien: Update to Versia 0.5 --- federation/http.ts | 67 +++++++++++++++++++++--- federation/schemas.ts | 9 +++- federation/schemas/base.ts | 36 ++++++------- federation/schemas/extensions/groups.ts | 40 +++++++++++++++ federation/schemas/extensions/vanity.ts | 7 ++- federation/schemas/regex.ts | 2 + federation/types.ts | 22 +++++++- federation/validator.ts | 68 +++++++++++++++++++++++-- 8 files changed, 220 insertions(+), 31 deletions(-) create mode 100644 federation/schemas/extensions/groups.ts diff --git a/federation/http.ts b/federation/http.ts index d96c16f..3e69265 100644 --- a/federation/http.ts +++ b/federation/http.ts @@ -4,7 +4,10 @@ import type { Follow, FollowAccept, FollowReject, - Group, + GroupExtensionSubscribe, + GroupExtensionSubscribeAccept, + GroupExtensionSubscribeReject, + GroupExtensionUnsubscribe, InstanceMetadata, LikeExtension, Note, @@ -28,7 +31,18 @@ type ParserCallbacks = { "pub.versia:likes/Dislike": (dislike: DislikeExtension) => MaybePromise; delete: (undo: Delete) => MaybePromise; instanceMetadata: (instanceMetadata: InstanceMetadata) => MaybePromise; - group: (group: Group) => MaybePromise; + "pub.versia:groups/Subscribe": ( + groupSubscribe: GroupExtensionSubscribe, + ) => MaybePromise; + "pub.versia:groups/SubscribeAccept": ( + groupSubscribeAccept: GroupExtensionSubscribeAccept, + ) => MaybePromise; + "pub.versia:groups/SubscribeReject": ( + groupSubscribeReject: GroupExtensionSubscribeReject, + ) => MaybePromise; + "pub.versia:groups/Unsubscribe": ( + groupUnsubscribe: GroupExtensionUnsubscribe, + ) => MaybePromise; "pub.versia:reactions/Reaction": ( reaction: ReactionExtension, ) => MaybePromise; @@ -182,11 +196,52 @@ export class RequestParserHandler { break; } - case "Group": { - const group = await this.validator.Group(this.body); + case "pub.versia:groups/Subscribe": { + const groupSubscribe = await this.validator.GroupSubscribe( + this.body, + ); - if (callbacks.group) { - return await callbacks.group(group); + if (callbacks["pub.versia:groups/Subscribe"]) { + return await callbacks["pub.versia:groups/Subscribe"]( + groupSubscribe, + ); + } + + break; + } + case "pub.versia:groups/SubscribeAccept": { + const groupSubscribeAccept = + await this.validator.GroupSubscribeAccept(this.body); + + if (callbacks["pub.versia:groups/SubscribeAccept"]) { + return await callbacks["pub.versia:groups/SubscribeAccept"]( + groupSubscribeAccept, + ); + } + + break; + } + case "pub.versia:groups/SubscribeReject": { + const groupSubscribeReject = + await this.validator.GroupSubscribeReject(this.body); + + if (callbacks["pub.versia:groups/SubscribeReject"]) { + return await callbacks["pub.versia:groups/SubscribeReject"]( + groupSubscribeReject, + ); + } + + break; + } + case "pub.versia:groups/Unsubscribe": { + const groupUnsubscribe = await this.validator.GroupUnsubscribe( + this.body, + ); + + if (callbacks["pub.versia:groups/Unsubscribe"]) { + return await callbacks["pub.versia:groups/Unsubscribe"]( + groupUnsubscribe, + ); } break; diff --git a/federation/schemas.ts b/federation/schemas.ts index 8d064cc..ae1ff53 100644 --- a/federation/schemas.ts +++ b/federation/schemas.ts @@ -11,7 +11,7 @@ export { FollowAcceptSchema as FollowAccept, FollowRejectSchema as FollowReject, FollowSchema as Follow, - GroupSchema as Group, + URICollectionSchema as URICollection, InstanceMetadataSchema as InstanceMetadata, NoteSchema as Note, UnfollowSchema as Unfollow, @@ -20,6 +20,13 @@ export { export { ContentFormatSchema as ContentFormat } from "./schemas/content_format.ts"; export { ExtensionPropertySchema as EntityExtensionProperty } from "./schemas/extensions.ts"; export { CustomEmojiExtensionSchema as CustomEmojiExtension } from "./schemas/extensions/custom_emojis.ts"; +export { + GroupSchema as GroupExtension, + GroupSubscribeSchema as GroupExtensionSubscribe, + GroupSubscribeAcceptSchema as GroupExtensionSubscribeAccept, + GroupSubscribeRejectSchema as GroupExtensionSubscribeReject, + GroupUnsubscribeSchema as GroupExtensionUnsubscribe, +} from "./schemas/extensions/groups.ts"; export { DislikeSchema as DislikeExtension, LikeSchema as LikeExtension, diff --git a/federation/schemas/base.ts b/federation/schemas/base.ts index ec7a64d..fb757e4 100644 --- a/federation/schemas/base.ts +++ b/federation/schemas/base.ts @@ -10,6 +10,8 @@ import { extensionRegex, isISOString, semverRegex } from "./regex.ts"; export const EntitySchema = z .object({ + // biome-ignore lint/style/useNamingConvention: + $schema: z.string().url().optional().nullable(), id: z.string().max(512), created_at: z .string() @@ -37,6 +39,17 @@ export const NoteSchema = EntitySchema.extend({ .optional() .nullable(), content: TextOnlyContentFormatSchema.optional().nullable(), + collections: z.object({ + replies: z.string().url(), + quotes: z.string().url(), + "pub.versia:reactions/Reactions": z + .string() + .url() + .optional() + .nullable(), + "pub.versia:likes/Likes": z.string().url().optional().nullable(), + "pub.versia:likes/Dislikes": z.string().url().optional().nullable(), + }), device: z .object({ name: z.string(), @@ -72,13 +85,6 @@ export const NoteSchema = EntitySchema.extend({ replies_to: z.string().url().optional().nullable(), subject: z.string().optional().nullable(), extensions: ExtensionPropertySchema.extend({ - "pub.versia:reactions": z - .object({ - reactions: z.string().url(), - }) - .strict() - .optional() - .nullable(), "pub.versia:polls": z .object({ options: z.array(TextOnlyContentFormatSchema), @@ -119,6 +125,10 @@ export const CollectionSchema = z.object({ items: z.array(z.any()), }); +export const URICollectionSchema = CollectionSchema.extend({ + items: z.array(z.string().url()), +}); + export const PublicKeyDataSchema = z .object({ key: z.string().min(1), @@ -147,8 +157,8 @@ export const UserSchema = EntitySchema.extend({ .string() .min(1) .regex( - /^[a-z0-9_-]+$/, - "must be lowercase, alphanumeric, and may contain _ or -", + /^[a-zA-Z0-9_-]+$/, + "must be alphanumeric, and may contain _ or -", ), header: ImageOnlyContentFormatSchema.optional().nullable(), public_key: PublicKeyDataSchema, @@ -208,14 +218,6 @@ export const UnfollowSchema = EntitySchema.extend({ followee: z.string().url(), }); -export const GroupSchema = EntitySchema.extend({ - type: z.literal("Group"), - name: TextOnlyContentFormatSchema.optional().nullable(), - description: TextOnlyContentFormatSchema.optional().nullable(), - members: z.string().url(), - notes: z.string().url().optional().nullable(), -}); - export const InstanceMetadataSchema = EntitySchema.extend({ type: z.literal("InstanceMetadata"), id: z.null().optional(), diff --git a/federation/schemas/extensions/groups.ts b/federation/schemas/extensions/groups.ts new file mode 100644 index 0000000..8c7222a --- /dev/null +++ b/federation/schemas/extensions/groups.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; +import { EntitySchema } from "../base.ts"; +import { TextOnlyContentFormatSchema } from "../content_format.ts"; + +export const GroupSchema = EntitySchema.extend({ + type: z.literal("pub.versia:groups/Group"), + name: TextOnlyContentFormatSchema.optional().nullable(), + description: TextOnlyContentFormatSchema.optional().nullable(), + open: z.boolean().optional().nullable(), + members: z.string().url(), + notes: z.string().url().optional().nullable(), +}); + +export const GroupSubscribeSchema = EntitySchema.extend({ + type: z.literal("pub.versia:groups/Subscribe"), + uri: z.null().optional(), + subscriber: z.string().url(), + group: z.string().url(), +}); + +export const GroupUnsubscribeSchema = EntitySchema.extend({ + type: z.literal("pub.versia:groups/Unsubscribe"), + uri: z.null().optional(), + subscriber: z.string().url(), + group: z.string().url(), +}); + +export const GroupSubscribeAcceptSchema = EntitySchema.extend({ + type: z.literal("pub.versia:groups/SubscribeAccept"), + uri: z.null().optional(), + subscriber: z.string().url(), + group: z.string().url(), +}); + +export const GroupSubscribeRejectSchema = EntitySchema.extend({ + type: z.literal("pub.versia:groups/SubscribeReject"), + uri: z.null().optional(), + subscriber: z.string().url(), + group: z.string().url(), +}); diff --git a/federation/schemas/extensions/vanity.ts b/federation/schemas/extensions/vanity.ts index c04cff9..31a0bdb 100644 --- a/federation/schemas/extensions/vanity.ts +++ b/federation/schemas/extensions/vanity.ts @@ -10,7 +10,7 @@ import { AudioOnlyContentFormatSchema, ImageOnlyContentFormatSchema, } from "../content_format.ts"; -import { isISOString } from "../regex.ts"; +import { ianaTimezoneRegex, isISOString } from "../regex.ts"; /** * @description Vanity extension entity @@ -103,5 +103,10 @@ export const VanityExtensionSchema = z .nullable(), location: z.string().optional().nullable(), aliases: z.array(z.string().url()).optional().nullable(), + timezone: z + .string() + .regex(ianaTimezoneRegex, "must be a valid IANA timezone") + .optional() + .nullable(), }) .strict(); diff --git a/federation/schemas/regex.ts b/federation/schemas/regex.ts index e169afd..dc082dc 100644 --- a/federation/schemas/regex.ts +++ b/federation/schemas/regex.ts @@ -66,3 +66,5 @@ export const isISOString = (val: string | Date) => { const d = new Date(val); return !Number.isNaN(d.valueOf()); }; + +export const ianaTimezoneRegex = /^(?:[A-Za-z]+(?:\/[A-Za-z_]+)+|UTC)$/; diff --git a/federation/types.ts b/federation/types.ts index daecd86..6f1402d 100644 --- a/federation/types.ts +++ b/federation/types.ts @@ -11,15 +11,22 @@ import type { FollowAcceptSchema, FollowRejectSchema, FollowSchema, - GroupSchema, InstanceMetadataSchema, NoteSchema, + URICollectionSchema, UnfollowSchema, UserSchema, } from "./schemas/base.ts"; import type { ContentFormatSchema } from "./schemas/content_format.ts"; import type { ExtensionPropertySchema } from "./schemas/extensions.ts"; import type { CustomEmojiExtensionSchema } from "./schemas/extensions/custom_emojis.ts"; +import type { + GroupSchema, + GroupSubscribeAcceptSchema, + GroupSubscribeRejectSchema, + GroupSubscribeSchema, + GroupUnsubscribeSchema, +} from "./schemas/extensions/groups.ts"; import type { DislikeSchema, LikeSchema } from "./schemas/extensions/likes.ts"; import type { VoteSchema } from "./schemas/extensions/polls.ts"; import type { ReactionSchema } from "./schemas/extensions/reactions.ts"; @@ -33,6 +40,7 @@ type InferType = z.infer; export type Note = InferType; export type Collection = InferType; +export type URICollection = InferType; export type EntityExtensionProperty = InferType; export type VanityExtension = InferType; export type User = InferType; @@ -43,9 +51,19 @@ export type ContentFormat = InferType; export type CustomEmojiExtension = InferType; export type Entity = InferType; export type Delete = InferType; -export type Group = InferType; export type InstanceMetadata = InferType; export type Unfollow = InferType; +export type GroupExtension = InferType; +export type GroupExtensionSubscribe = InferType; +export type GroupExtensionSubscribeAccept = InferType< + typeof GroupSubscribeAcceptSchema +>; +export type GroupExtensionSubscribeReject = InferType< + typeof GroupSubscribeRejectSchema +>; +export type GroupExtensionUnsubscribe = InferType< + typeof GroupUnsubscribeSchema +>; export type LikeExtension = InferType; export type DislikeExtension = InferType; export type PollVoteExtension = InferType; diff --git a/federation/validator.ts b/federation/validator.ts index da5fc37..e11f58e 100644 --- a/federation/validator.ts +++ b/federation/validator.ts @@ -7,15 +7,22 @@ import { FollowAcceptSchema, FollowRejectSchema, FollowSchema, - GroupSchema, InstanceMetadataSchema, NoteSchema, + URICollectionSchema, UnfollowSchema, UserSchema, } from "./schemas/base.ts"; import { ContentFormatSchema } from "./schemas/content_format.ts"; import { ExtensionPropertySchema } from "./schemas/extensions.ts"; import { CustomEmojiExtensionSchema } from "./schemas/extensions/custom_emojis.ts"; +import { + GroupSchema, + GroupSubscribeAcceptSchema, + GroupSubscribeRejectSchema, + GroupSubscribeSchema, + GroupUnsubscribeSchema, +} from "./schemas/extensions/groups.ts"; import { DislikeSchema, LikeSchema } from "./schemas/extensions/likes.ts"; import { VoteSchema } from "./schemas/extensions/polls.ts"; import { ReactionSchema } from "./schemas/extensions/reactions.ts"; @@ -32,18 +39,22 @@ import type { Follow, FollowAccept, FollowReject, - Group, + GroupExtension, + GroupExtensionSubscribe, + GroupExtensionSubscribeAccept, + GroupExtensionSubscribeReject, + GroupExtensionUnsubscribe, InstanceMetadata, LikeExtension, Note, PollVoteExtension, ReactionExtension, ShareExtension, + URICollection, Unfollow, User, VanityExtension, } from "./types.ts"; - // biome-ignore lint/suspicious/noExplicitAny: Used only as a base type type AnyZod = z.ZodType; @@ -110,6 +121,15 @@ export class EntityValidator { return this.validate(CollectionSchema, data); } + /** + * Validates a URICollection entity. + * @param data - The data to validate + * @returns A promise that resolves to the validated data. + */ + public URICollection(data: unknown): Promise { + return this.validate(URICollectionSchema, data); + } + /** * Validates a VanityExtension entity. * @param data - The data to validate @@ -207,10 +227,50 @@ export class EntityValidator { * @param data - The data to validate * @returns A promise that resolves to the validated data. */ - public Group(data: unknown): Promise { + public Group(data: unknown): Promise { return this.validate(GroupSchema, data); } + /** + * Validates a GroupSubscribe entity. + * @param data - The data to validate + * @returns A promise that resolves to the validated data. + */ + public GroupSubscribe(data: unknown): Promise { + return this.validate(GroupSubscribeSchema, data); + } + + /** + * Validates a GroupSubscribeAccept entity. + * @param data - The data to validate + * @returns A promise that resolves to the validated data. + */ + public GroupSubscribeAccept( + data: unknown, + ): Promise { + return this.validate(GroupSubscribeAcceptSchema, data); + } + + /** + * Validates a GroupSubscribeReject entity. + * @param data - The data to validate + * @returns A promise that resolves to the validated data. + */ + public GroupSubscribeReject( + data: unknown, + ): Promise { + return this.validate(GroupSubscribeRejectSchema, data); + } + + /** + * Validates a GroupUnsubscribe entity. + * @param data - The data to validate + * @returns A promise that resolves to the validated data. + */ + public GroupUnsubscribe(data: unknown): Promise { + return this.validate(GroupUnsubscribeSchema, data); + } + /** * Validates an InstanceMetadata entity. * @param data - The data to validate