feat(federation): 👽 Update to Versia 0.5

This commit is contained in:
Jesse Wierzbinski 2025-02-13 18:03:04 +01:00
parent afec384a51
commit 5114df4454
No known key found for this signature in database
8 changed files with 220 additions and 31 deletions

View file

@ -4,7 +4,10 @@ import type {
Follow, Follow,
FollowAccept, FollowAccept,
FollowReject, FollowReject,
Group, GroupExtensionSubscribe,
GroupExtensionSubscribeAccept,
GroupExtensionSubscribeReject,
GroupExtensionUnsubscribe,
InstanceMetadata, InstanceMetadata,
LikeExtension, LikeExtension,
Note, Note,
@ -28,7 +31,18 @@ type ParserCallbacks<T> = {
"pub.versia:likes/Dislike": (dislike: DislikeExtension) => MaybePromise<T>; "pub.versia:likes/Dislike": (dislike: DislikeExtension) => MaybePromise<T>;
delete: (undo: Delete) => MaybePromise<T>; delete: (undo: Delete) => MaybePromise<T>;
instanceMetadata: (instanceMetadata: InstanceMetadata) => MaybePromise<T>; instanceMetadata: (instanceMetadata: InstanceMetadata) => MaybePromise<T>;
group: (group: Group) => MaybePromise<T>; "pub.versia:groups/Subscribe": (
groupSubscribe: GroupExtensionSubscribe,
) => MaybePromise<T>;
"pub.versia:groups/SubscribeAccept": (
groupSubscribeAccept: GroupExtensionSubscribeAccept,
) => MaybePromise<T>;
"pub.versia:groups/SubscribeReject": (
groupSubscribeReject: GroupExtensionSubscribeReject,
) => MaybePromise<T>;
"pub.versia:groups/Unsubscribe": (
groupUnsubscribe: GroupExtensionUnsubscribe,
) => MaybePromise<T>;
"pub.versia:reactions/Reaction": ( "pub.versia:reactions/Reaction": (
reaction: ReactionExtension, reaction: ReactionExtension,
) => MaybePromise<T>; ) => MaybePromise<T>;
@ -182,11 +196,52 @@ export class RequestParserHandler {
break; break;
} }
case "Group": { case "pub.versia:groups/Subscribe": {
const group = await this.validator.Group(this.body); const groupSubscribe = await this.validator.GroupSubscribe(
this.body,
);
if (callbacks.group) { if (callbacks["pub.versia:groups/Subscribe"]) {
return await callbacks.group(group); 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; break;

View file

@ -11,7 +11,7 @@ export {
FollowAcceptSchema as FollowAccept, FollowAcceptSchema as FollowAccept,
FollowRejectSchema as FollowReject, FollowRejectSchema as FollowReject,
FollowSchema as Follow, FollowSchema as Follow,
GroupSchema as Group, URICollectionSchema as URICollection,
InstanceMetadataSchema as InstanceMetadata, InstanceMetadataSchema as InstanceMetadata,
NoteSchema as Note, NoteSchema as Note,
UnfollowSchema as Unfollow, UnfollowSchema as Unfollow,
@ -20,6 +20,13 @@ export {
export { ContentFormatSchema as ContentFormat } from "./schemas/content_format.ts"; export { ContentFormatSchema as ContentFormat } from "./schemas/content_format.ts";
export { ExtensionPropertySchema as EntityExtensionProperty } from "./schemas/extensions.ts"; export { ExtensionPropertySchema as EntityExtensionProperty } from "./schemas/extensions.ts";
export { CustomEmojiExtensionSchema as CustomEmojiExtension } from "./schemas/extensions/custom_emojis.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 { export {
DislikeSchema as DislikeExtension, DislikeSchema as DislikeExtension,
LikeSchema as LikeExtension, LikeSchema as LikeExtension,

View file

@ -10,6 +10,8 @@ import { extensionRegex, isISOString, semverRegex } from "./regex.ts";
export const EntitySchema = z export const EntitySchema = z
.object({ .object({
// biome-ignore lint/style/useNamingConvention:
$schema: z.string().url().optional().nullable(),
id: z.string().max(512), id: z.string().max(512),
created_at: z created_at: z
.string() .string()
@ -37,6 +39,17 @@ export const NoteSchema = EntitySchema.extend({
.optional() .optional()
.nullable(), .nullable(),
content: TextOnlyContentFormatSchema.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 device: z
.object({ .object({
name: z.string(), name: z.string(),
@ -72,13 +85,6 @@ export const NoteSchema = EntitySchema.extend({
replies_to: z.string().url().optional().nullable(), replies_to: z.string().url().optional().nullable(),
subject: z.string().optional().nullable(), subject: z.string().optional().nullable(),
extensions: ExtensionPropertySchema.extend({ extensions: ExtensionPropertySchema.extend({
"pub.versia:reactions": z
.object({
reactions: z.string().url(),
})
.strict()
.optional()
.nullable(),
"pub.versia:polls": z "pub.versia:polls": z
.object({ .object({
options: z.array(TextOnlyContentFormatSchema), options: z.array(TextOnlyContentFormatSchema),
@ -119,6 +125,10 @@ export const CollectionSchema = z.object({
items: z.array(z.any()), items: z.array(z.any()),
}); });
export const URICollectionSchema = CollectionSchema.extend({
items: z.array(z.string().url()),
});
export const PublicKeyDataSchema = z export const PublicKeyDataSchema = z
.object({ .object({
key: z.string().min(1), key: z.string().min(1),
@ -147,8 +157,8 @@ export const UserSchema = EntitySchema.extend({
.string() .string()
.min(1) .min(1)
.regex( .regex(
/^[a-z0-9_-]+$/, /^[a-zA-Z0-9_-]+$/,
"must be lowercase, alphanumeric, and may contain _ or -", "must be alphanumeric, and may contain _ or -",
), ),
header: ImageOnlyContentFormatSchema.optional().nullable(), header: ImageOnlyContentFormatSchema.optional().nullable(),
public_key: PublicKeyDataSchema, public_key: PublicKeyDataSchema,
@ -208,14 +218,6 @@ export const UnfollowSchema = EntitySchema.extend({
followee: z.string().url(), 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({ export const InstanceMetadataSchema = EntitySchema.extend({
type: z.literal("InstanceMetadata"), type: z.literal("InstanceMetadata"),
id: z.null().optional(), id: z.null().optional(),

View file

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

View file

@ -10,7 +10,7 @@ import {
AudioOnlyContentFormatSchema, AudioOnlyContentFormatSchema,
ImageOnlyContentFormatSchema, ImageOnlyContentFormatSchema,
} from "../content_format.ts"; } from "../content_format.ts";
import { isISOString } from "../regex.ts"; import { ianaTimezoneRegex, isISOString } from "../regex.ts";
/** /**
* @description Vanity extension entity * @description Vanity extension entity
@ -103,5 +103,10 @@ export const VanityExtensionSchema = z
.nullable(), .nullable(),
location: z.string().optional().nullable(), location: z.string().optional().nullable(),
aliases: z.array(z.string().url()).optional().nullable(), aliases: z.array(z.string().url()).optional().nullable(),
timezone: z
.string()
.regex(ianaTimezoneRegex, "must be a valid IANA timezone")
.optional()
.nullable(),
}) })
.strict(); .strict();

View file

@ -66,3 +66,5 @@ export const isISOString = (val: string | Date) => {
const d = new Date(val); const d = new Date(val);
return !Number.isNaN(d.valueOf()); return !Number.isNaN(d.valueOf());
}; };
export const ianaTimezoneRegex = /^(?:[A-Za-z]+(?:\/[A-Za-z_]+)+|UTC)$/;

View file

@ -11,15 +11,22 @@ import type {
FollowAcceptSchema, FollowAcceptSchema,
FollowRejectSchema, FollowRejectSchema,
FollowSchema, FollowSchema,
GroupSchema,
InstanceMetadataSchema, InstanceMetadataSchema,
NoteSchema, NoteSchema,
URICollectionSchema,
UnfollowSchema, UnfollowSchema,
UserSchema, UserSchema,
} from "./schemas/base.ts"; } from "./schemas/base.ts";
import type { ContentFormatSchema } from "./schemas/content_format.ts"; import type { ContentFormatSchema } from "./schemas/content_format.ts";
import type { ExtensionPropertySchema } from "./schemas/extensions.ts"; import type { ExtensionPropertySchema } from "./schemas/extensions.ts";
import type { CustomEmojiExtensionSchema } from "./schemas/extensions/custom_emojis.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 { DislikeSchema, LikeSchema } from "./schemas/extensions/likes.ts";
import type { VoteSchema } from "./schemas/extensions/polls.ts"; import type { VoteSchema } from "./schemas/extensions/polls.ts";
import type { ReactionSchema } from "./schemas/extensions/reactions.ts"; import type { ReactionSchema } from "./schemas/extensions/reactions.ts";
@ -33,6 +40,7 @@ type InferType<T extends AnyZod> = z.infer<T>;
export type Note = InferType<typeof NoteSchema>; export type Note = InferType<typeof NoteSchema>;
export type Collection = InferType<typeof CollectionSchema>; export type Collection = InferType<typeof CollectionSchema>;
export type URICollection = InferType<typeof URICollectionSchema>;
export type EntityExtensionProperty = InferType<typeof ExtensionPropertySchema>; export type EntityExtensionProperty = InferType<typeof ExtensionPropertySchema>;
export type VanityExtension = InferType<typeof VanityExtensionSchema>; export type VanityExtension = InferType<typeof VanityExtensionSchema>;
export type User = InferType<typeof UserSchema>; export type User = InferType<typeof UserSchema>;
@ -43,9 +51,19 @@ export type ContentFormat = InferType<typeof ContentFormatSchema>;
export type CustomEmojiExtension = InferType<typeof CustomEmojiExtensionSchema>; export type CustomEmojiExtension = InferType<typeof CustomEmojiExtensionSchema>;
export type Entity = InferType<typeof EntitySchema>; export type Entity = InferType<typeof EntitySchema>;
export type Delete = InferType<typeof DeleteSchema>; export type Delete = InferType<typeof DeleteSchema>;
export type Group = InferType<typeof GroupSchema>;
export type InstanceMetadata = InferType<typeof InstanceMetadataSchema>; export type InstanceMetadata = InferType<typeof InstanceMetadataSchema>;
export type Unfollow = InferType<typeof UnfollowSchema>; export type Unfollow = InferType<typeof UnfollowSchema>;
export type GroupExtension = InferType<typeof GroupSchema>;
export type GroupExtensionSubscribe = InferType<typeof GroupSubscribeSchema>;
export type GroupExtensionSubscribeAccept = InferType<
typeof GroupSubscribeAcceptSchema
>;
export type GroupExtensionSubscribeReject = InferType<
typeof GroupSubscribeRejectSchema
>;
export type GroupExtensionUnsubscribe = InferType<
typeof GroupUnsubscribeSchema
>;
export type LikeExtension = InferType<typeof LikeSchema>; export type LikeExtension = InferType<typeof LikeSchema>;
export type DislikeExtension = InferType<typeof DislikeSchema>; export type DislikeExtension = InferType<typeof DislikeSchema>;
export type PollVoteExtension = InferType<typeof VoteSchema>; export type PollVoteExtension = InferType<typeof VoteSchema>;

View file

@ -7,15 +7,22 @@ import {
FollowAcceptSchema, FollowAcceptSchema,
FollowRejectSchema, FollowRejectSchema,
FollowSchema, FollowSchema,
GroupSchema,
InstanceMetadataSchema, InstanceMetadataSchema,
NoteSchema, NoteSchema,
URICollectionSchema,
UnfollowSchema, UnfollowSchema,
UserSchema, UserSchema,
} from "./schemas/base.ts"; } from "./schemas/base.ts";
import { ContentFormatSchema } from "./schemas/content_format.ts"; import { ContentFormatSchema } from "./schemas/content_format.ts";
import { ExtensionPropertySchema } from "./schemas/extensions.ts"; import { ExtensionPropertySchema } from "./schemas/extensions.ts";
import { CustomEmojiExtensionSchema } from "./schemas/extensions/custom_emojis.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 { DislikeSchema, LikeSchema } from "./schemas/extensions/likes.ts";
import { VoteSchema } from "./schemas/extensions/polls.ts"; import { VoteSchema } from "./schemas/extensions/polls.ts";
import { ReactionSchema } from "./schemas/extensions/reactions.ts"; import { ReactionSchema } from "./schemas/extensions/reactions.ts";
@ -32,18 +39,22 @@ import type {
Follow, Follow,
FollowAccept, FollowAccept,
FollowReject, FollowReject,
Group, GroupExtension,
GroupExtensionSubscribe,
GroupExtensionSubscribeAccept,
GroupExtensionSubscribeReject,
GroupExtensionUnsubscribe,
InstanceMetadata, InstanceMetadata,
LikeExtension, LikeExtension,
Note, Note,
PollVoteExtension, PollVoteExtension,
ReactionExtension, ReactionExtension,
ShareExtension, ShareExtension,
URICollection,
Unfollow, Unfollow,
User, User,
VanityExtension, VanityExtension,
} from "./types.ts"; } from "./types.ts";
// biome-ignore lint/suspicious/noExplicitAny: Used only as a base type // biome-ignore lint/suspicious/noExplicitAny: Used only as a base type
type AnyZod = z.ZodType<any, any, any>; type AnyZod = z.ZodType<any, any, any>;
@ -110,6 +121,15 @@ export class EntityValidator {
return this.validate(CollectionSchema, data); 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<URICollection> {
return this.validate(URICollectionSchema, data);
}
/** /**
* Validates a VanityExtension entity. * Validates a VanityExtension entity.
* @param data - The data to validate * @param data - The data to validate
@ -207,10 +227,50 @@ export class EntityValidator {
* @param data - The data to validate * @param data - The data to validate
* @returns A promise that resolves to the validated data. * @returns A promise that resolves to the validated data.
*/ */
public Group(data: unknown): Promise<Group> { public Group(data: unknown): Promise<GroupExtension> {
return this.validate(GroupSchema, data); 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<GroupExtensionSubscribe> {
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<GroupExtensionSubscribeAccept> {
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<GroupExtensionSubscribeReject> {
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<GroupExtensionUnsubscribe> {
return this.validate(GroupUnsubscribeSchema, data);
}
/** /**
* Validates an InstanceMetadata entity. * Validates an InstanceMetadata entity.
* @param data - The data to validate * @param data - The data to validate