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,
FollowAccept,
FollowReject,
Group,
GroupExtensionSubscribe,
GroupExtensionSubscribeAccept,
GroupExtensionSubscribeReject,
GroupExtensionUnsubscribe,
InstanceMetadata,
LikeExtension,
Note,
@ -28,7 +31,18 @@ type ParserCallbacks<T> = {
"pub.versia:likes/Dislike": (dislike: DislikeExtension) => MaybePromise<T>;
delete: (undo: Delete) => 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": (
reaction: ReactionExtension,
) => MaybePromise<T>;
@ -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;

View file

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

View file

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

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

View file

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

View file

@ -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<T extends AnyZod> = z.infer<T>;
export type Note = InferType<typeof NoteSchema>;
export type Collection = InferType<typeof CollectionSchema>;
export type URICollection = InferType<typeof URICollectionSchema>;
export type EntityExtensionProperty = InferType<typeof ExtensionPropertySchema>;
export type VanityExtension = InferType<typeof VanityExtensionSchema>;
export type User = InferType<typeof UserSchema>;
@ -43,9 +51,19 @@ export type ContentFormat = InferType<typeof ContentFormatSchema>;
export type CustomEmojiExtension = InferType<typeof CustomEmojiExtensionSchema>;
export type Entity = InferType<typeof EntitySchema>;
export type Delete = InferType<typeof DeleteSchema>;
export type Group = InferType<typeof GroupSchema>;
export type InstanceMetadata = InferType<typeof InstanceMetadataSchema>;
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 DislikeExtension = InferType<typeof DislikeSchema>;
export type PollVoteExtension = InferType<typeof VoteSchema>;

View file

@ -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<any, any, any>;
@ -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<URICollection> {
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<Group> {
public Group(data: unknown): Promise<GroupExtension> {
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.
* @param data - The data to validate