mirror of
https://github.com/versia-pub/server.git
synced 2026-04-27 20:59:15 +02:00
feat(federation): ✨ Port to Versia 0.6
This commit is contained in:
parent
de69f27877
commit
fca30b4dad
62 changed files with 1614 additions and 2008 deletions
|
|
@ -13,15 +13,15 @@ const base64ToArrayBuffer = (base64: string): ArrayBuffer =>
|
|||
* Signs a request using the Ed25519 algorithm, according to the [**Versia**](https://versia.pub/signatures) specification.
|
||||
*
|
||||
* @see https://versia.pub/signatures
|
||||
* @param privateKey - Private key of the User that is signing the request.
|
||||
* @param authorUrl - URL of the User that is signing the request.
|
||||
* @param privateKey - Private key of the instance that is signing the request.
|
||||
* @param instance - URL of the instance that is signing the request.
|
||||
* @param req - Request to sign.
|
||||
* @param timestamp - (optional) Timestamp of the request.
|
||||
* @returns The signed request.
|
||||
*/
|
||||
export const sign = async (
|
||||
privateKey: CryptoKey,
|
||||
authorUrl: URL,
|
||||
instance: URL,
|
||||
req: Request,
|
||||
timestamp = new Date(),
|
||||
): Promise<Request> => {
|
||||
|
|
@ -48,7 +48,7 @@ export const sign = async (
|
|||
...req.headers,
|
||||
"Versia-Signature": signatureBase64,
|
||||
"Versia-Signed-At": String(timestampSecs),
|
||||
"Versia-Signed-By": authorUrl.href,
|
||||
"Versia-Signed-By": instance.hostname,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { z } from "zod";
|
||||
import { DeleteSchema } from "../schemas/delete.ts";
|
||||
import type { JSONObject } from "../types.ts";
|
||||
import { Entity } from "./entity.ts";
|
||||
import { Entity, Reference } from "./entity.ts";
|
||||
|
||||
export class Delete extends Entity {
|
||||
public static override name = "Delete";
|
||||
|
|
@ -10,6 +10,14 @@ export class Delete extends Entity {
|
|||
super(data);
|
||||
}
|
||||
|
||||
public get author(): Reference {
|
||||
return Reference.fromString(this.data.author);
|
||||
}
|
||||
|
||||
public get deleted(): Reference {
|
||||
return Reference.fromString(this.data.deleted);
|
||||
}
|
||||
|
||||
public static override fromJSON(json: JSONObject): Promise<Delete> {
|
||||
return DeleteSchema.parseAsync(json).then((u) => new Delete(u));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,3 +15,31 @@ export class Entity {
|
|||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
export class Reference {
|
||||
public constructor(
|
||||
public id: string,
|
||||
public domain?: string,
|
||||
) {}
|
||||
|
||||
public static fromString(str: string): Reference {
|
||||
// Expect format: domain:id or id (if domain is the local instance)
|
||||
// Handle IPv6 addresses in brackets
|
||||
const chunks = str.split(":");
|
||||
if (chunks.length === 2) {
|
||||
return new Reference(chunks[1], chunks[0]);
|
||||
}
|
||||
|
||||
if (chunks.length > 2) {
|
||||
const domain = chunks.slice(0, -1).join(":");
|
||||
const id = chunks.at(-1) as string;
|
||||
return new Reference(id, domain);
|
||||
}
|
||||
|
||||
return new Reference(str);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.domain ? `${this.domain}:${this.id}` : this.id;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { z } from "zod";
|
||||
import { DislikeSchema, LikeSchema } from "../../schemas/extensions/likes.ts";
|
||||
import type { JSONObject } from "../../types.ts";
|
||||
import { Entity } from "../entity.ts";
|
||||
import { Entity, Reference } from "../entity.ts";
|
||||
|
||||
export class Like extends Entity {
|
||||
public static override name = "pub.versia:likes/Like";
|
||||
|
|
@ -10,6 +10,14 @@ export class Like extends Entity {
|
|||
super(data);
|
||||
}
|
||||
|
||||
public get author(): Reference {
|
||||
return Reference.fromString(this.data.author);
|
||||
}
|
||||
|
||||
public get liked(): Reference {
|
||||
return Reference.fromString(this.data.liked);
|
||||
}
|
||||
|
||||
public static override fromJSON(json: JSONObject): Promise<Like> {
|
||||
return LikeSchema.parseAsync(json).then((u) => new Like(u));
|
||||
}
|
||||
|
|
@ -22,6 +30,14 @@ export class Dislike extends Entity {
|
|||
super(data);
|
||||
}
|
||||
|
||||
public get author(): Reference {
|
||||
return Reference.fromString(this.data.author);
|
||||
}
|
||||
|
||||
public get disliked(): Reference {
|
||||
return Reference.fromString(this.data.disliked);
|
||||
}
|
||||
|
||||
public static override fromJSON(json: JSONObject): Promise<Dislike> {
|
||||
return DislikeSchema.parseAsync(json).then((u) => new Dislike(u));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { z } from "zod";
|
||||
import { VoteSchema } from "../../schemas/extensions/polls.ts";
|
||||
import type { JSONObject } from "../../types.ts";
|
||||
import { Entity } from "../entity.ts";
|
||||
import { Entity, Reference } from "../entity.ts";
|
||||
|
||||
export class Vote extends Entity {
|
||||
public static override name = "pub.versia:polls/Vote";
|
||||
|
|
@ -10,6 +10,14 @@ export class Vote extends Entity {
|
|||
super(data);
|
||||
}
|
||||
|
||||
public get author(): Reference {
|
||||
return Reference.fromString(this.data.author);
|
||||
}
|
||||
|
||||
public get poll(): Reference {
|
||||
return Reference.fromString(this.data.poll);
|
||||
}
|
||||
|
||||
public static override fromJSON(json: JSONObject): Promise<Vote> {
|
||||
return VoteSchema.parseAsync(json).then((u) => new Vote(u));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { z } from "zod";
|
||||
import { ReactionSchema } from "../../schemas/extensions/reactions.ts";
|
||||
import type { JSONObject } from "../../types.ts";
|
||||
import { Entity } from "../entity.ts";
|
||||
import { Entity, Reference } from "../entity.ts";
|
||||
|
||||
export class Reaction extends Entity {
|
||||
public static override name = "pub.versia:reactions/Reaction";
|
||||
|
|
@ -10,6 +10,14 @@ export class Reaction extends Entity {
|
|||
super(data);
|
||||
}
|
||||
|
||||
public get author(): Reference {
|
||||
return Reference.fromString(this.data.author);
|
||||
}
|
||||
|
||||
public get object(): Reference {
|
||||
return Reference.fromString(this.data.object);
|
||||
}
|
||||
|
||||
public static override fromJSON(json: JSONObject): Promise<Reaction> {
|
||||
return ReactionSchema.parseAsync(json).then((u) => new Reaction(u));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { z } from "zod";
|
||||
import { ReportSchema } from "../../schemas/extensions/reports.ts";
|
||||
import type { JSONObject } from "../../types.ts";
|
||||
import { Entity } from "../entity.ts";
|
||||
import { Entity, Reference } from "../entity.ts";
|
||||
|
||||
export class Report extends Entity {
|
||||
public static override name = "pub.versia:reports/Report";
|
||||
|
|
@ -10,6 +10,14 @@ export class Report extends Entity {
|
|||
super(data);
|
||||
}
|
||||
|
||||
public get author(): Reference | null {
|
||||
return this.data.author ? Reference.fromString(this.data.author) : null;
|
||||
}
|
||||
|
||||
public get reported(): Reference[] {
|
||||
return this.data.reported.map((r) => Reference.fromString(r));
|
||||
}
|
||||
|
||||
public static override fromJSON(json: JSONObject): Promise<Report> {
|
||||
return ReportSchema.parseAsync(json).then((u) => new Report(u));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { z } from "zod";
|
||||
import { ShareSchema } from "../../schemas/extensions/share.ts";
|
||||
import type { JSONObject } from "../../types.ts";
|
||||
import { Entity } from "../entity.ts";
|
||||
import { Entity, Reference } from "../entity.ts";
|
||||
|
||||
export class Share extends Entity {
|
||||
public static override name = "pub.versia:share/Share";
|
||||
|
|
@ -10,6 +10,14 @@ export class Share extends Entity {
|
|||
super(data);
|
||||
}
|
||||
|
||||
public get author(): Reference {
|
||||
return Reference.fromString(this.data.author);
|
||||
}
|
||||
|
||||
public get shared(): Reference {
|
||||
return Reference.fromString(this.data.shared);
|
||||
}
|
||||
|
||||
public static override fromJSON(json: JSONObject): Promise<Share> {
|
||||
return ShareSchema.parseAsync(json).then((u) => new Share(u));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
UnfollowSchema,
|
||||
} from "../schemas/follow.ts";
|
||||
import type { JSONObject } from "../types.ts";
|
||||
import { Entity } from "./entity.ts";
|
||||
import { Entity, Reference } from "./entity.ts";
|
||||
|
||||
export class Follow extends Entity {
|
||||
public static override name = "Follow";
|
||||
|
|
@ -15,6 +15,14 @@ export class Follow extends Entity {
|
|||
super(data);
|
||||
}
|
||||
|
||||
public get author(): Reference {
|
||||
return Reference.fromString(this.data.author);
|
||||
}
|
||||
|
||||
public get followee(): Reference {
|
||||
return Reference.fromString(this.data.followee);
|
||||
}
|
||||
|
||||
public static override fromJSON(json: JSONObject): Promise<Follow> {
|
||||
return FollowSchema.parseAsync(json).then((u) => new Follow(u));
|
||||
}
|
||||
|
|
@ -29,6 +37,14 @@ export class FollowAccept extends Entity {
|
|||
super(data);
|
||||
}
|
||||
|
||||
public get author(): Reference {
|
||||
return Reference.fromString(this.data.author);
|
||||
}
|
||||
|
||||
public get follower(): Reference {
|
||||
return Reference.fromString(this.data.follower);
|
||||
}
|
||||
|
||||
public static override fromJSON(json: JSONObject): Promise<FollowAccept> {
|
||||
return FollowAcceptSchema.parseAsync(json).then(
|
||||
(u) => new FollowAccept(u),
|
||||
|
|
@ -45,6 +61,14 @@ export class FollowReject extends Entity {
|
|||
super(data);
|
||||
}
|
||||
|
||||
public get author(): Reference {
|
||||
return Reference.fromString(this.data.author);
|
||||
}
|
||||
|
||||
public get follower(): Reference {
|
||||
return Reference.fromString(this.data.follower);
|
||||
}
|
||||
|
||||
public static override fromJSON(json: JSONObject): Promise<FollowReject> {
|
||||
return FollowRejectSchema.parseAsync(json).then(
|
||||
(u) => new FollowReject(u),
|
||||
|
|
@ -59,6 +83,14 @@ export class Unfollow extends Entity {
|
|||
super(data);
|
||||
}
|
||||
|
||||
public get author(): Reference {
|
||||
return Reference.fromString(this.data.author);
|
||||
}
|
||||
|
||||
public get followee(): Reference {
|
||||
return Reference.fromString(this.data.followee);
|
||||
}
|
||||
|
||||
public static override fromJSON(json: JSONObject): Promise<Unfollow> {
|
||||
return UnfollowSchema.parseAsync(json).then((u) => new Unfollow(u));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export {
|
|||
VideoContentFormat,
|
||||
} from "./contentformat.ts";
|
||||
export { Delete } from "./delete.ts";
|
||||
export { Entity } from "./entity.ts";
|
||||
export { Entity, Reference } from "./entity.ts";
|
||||
export { Dislike, Like } from "./extensions/likes.ts";
|
||||
export { Vote } from "./extensions/polls.ts";
|
||||
export { Reaction } from "./extensions/reactions.ts";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { z } from "zod";
|
|||
import { NoteSchema } from "../schemas/note.ts";
|
||||
import type { JSONObject } from "../types.ts";
|
||||
import { NonTextContentFormat, TextContentFormat } from "./contentformat.ts";
|
||||
import { Entity } from "./entity.ts";
|
||||
import { Entity, Reference } from "./entity.ts";
|
||||
|
||||
export class Note extends Entity {
|
||||
public static override name = "Note";
|
||||
|
|
@ -15,6 +15,35 @@ export class Note extends Entity {
|
|||
return NoteSchema.parseAsync(json).then((n) => new Note(n));
|
||||
}
|
||||
|
||||
public get author(): Reference {
|
||||
return Reference.fromString(this.data.author);
|
||||
}
|
||||
|
||||
public get group(): Reference | null {
|
||||
if (
|
||||
!this.data.group ||
|
||||
["public", "followers"].includes(this.data.group)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Reference.fromString(this.data.group);
|
||||
}
|
||||
|
||||
public get mentions(): Reference[] {
|
||||
return this.data.mentions.map((m) => Reference.fromString(m));
|
||||
}
|
||||
|
||||
public get quotes(): Reference | null {
|
||||
return this.data.quotes ? Reference.fromString(this.data.quotes) : null;
|
||||
}
|
||||
|
||||
public get repliesTo(): Reference | null {
|
||||
return this.data.replies_to
|
||||
? Reference.fromString(this.data.replies_to)
|
||||
: null;
|
||||
}
|
||||
|
||||
public get attachments(): NonTextContentFormat[] {
|
||||
return (
|
||||
this.data.attachments?.map((a) => new NonTextContentFormat(a)) ?? []
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { sign } from "./crypto.ts";
|
||||
import { Collection, URICollection } from "./entities/collection.ts";
|
||||
import type { Entity } from "./entities/entity.ts";
|
||||
import type { Entity, Reference } from "./entities/entity.ts";
|
||||
import { InstanceMetadata } from "./entities/instancemetadata.ts";
|
||||
import { homepage, version } from "./package.json" with { type: "json" };
|
||||
import { WebFingerSchema } from "./schemas/webfinger.ts";
|
||||
|
||||
const DEFAULT_UA = `VersiaFederationClient/${version} (+${homepage})`;
|
||||
const CONTENT_TYPE = "application/vnd.versia+json";
|
||||
|
||||
/**
|
||||
* A class that handles fetching Versia entities
|
||||
|
|
@ -22,22 +24,22 @@ const DEFAULT_UA = `VersiaFederationClient/${version} (+${homepage})`;
|
|||
export class FederationRequester {
|
||||
public constructor(
|
||||
private readonly privateKey: CryptoKey,
|
||||
private readonly authorUrl: URL,
|
||||
private readonly instance: URL,
|
||||
) {}
|
||||
|
||||
public async fetchEntity<T extends typeof Entity>(
|
||||
public async fetchSigned<T extends typeof Entity>(
|
||||
url: URL,
|
||||
expectedType: T,
|
||||
entityType: T,
|
||||
): Promise<InstanceType<T>> {
|
||||
const req = new Request(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Accept: CONTENT_TYPE,
|
||||
"User-Agent": DEFAULT_UA,
|
||||
},
|
||||
});
|
||||
|
||||
const finalReq = await sign(this.privateKey, this.authorUrl, req);
|
||||
const finalReq = await sign(this.privateKey, this.instance, req);
|
||||
|
||||
const res = await fetch(finalReq);
|
||||
|
||||
|
|
@ -49,79 +51,116 @@ export class FederationRequester {
|
|||
|
||||
const contentType = res.headers.get("Content-Type");
|
||||
|
||||
if (!contentType?.includes("application/json")) {
|
||||
if (
|
||||
!(
|
||||
contentType?.includes("application/vnd.versia+json") &&
|
||||
contentType?.includes("charset=utf-8")
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Expected JSON response from ${url.toString()}, got "${contentType}"`,
|
||||
`Expected application/vnd.versia+json; charset=utf-8 response from ${url.toString()}, got "${contentType}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const jsonData = await res.json();
|
||||
const type = jsonData.type;
|
||||
|
||||
if (type && type !== expectedType.name) {
|
||||
if (
|
||||
(!type || type !== entityType.name) &&
|
||||
// (URI)Collections don't have a type field
|
||||
![Collection, URICollection].some((et) => et === entityType)
|
||||
) {
|
||||
throw new Error(
|
||||
`Expected entity type "${expectedType.name}", got "${type}"`,
|
||||
`Expected entity type "${entityType.name}", got "${type}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const entity = await expectedType.fromJSON(jsonData);
|
||||
const entity = await entityType.fromJSON(jsonData);
|
||||
|
||||
return entity as InstanceType<T>;
|
||||
}
|
||||
|
||||
public async postEntity(url: URL, entity: Entity): Promise<Response> {
|
||||
public fetchEntity<T extends typeof Entity>(
|
||||
reference: Reference,
|
||||
entityType: T,
|
||||
): Promise<InstanceType<T>> {
|
||||
const url = new URL(
|
||||
`/.versia/v0.6/entities/${encodeURIComponent(
|
||||
entityType.name,
|
||||
)}/${encodeURIComponent(reference.id)}`,
|
||||
`https://${reference.domain}`,
|
||||
);
|
||||
|
||||
return this.fetchSigned(url, entityType);
|
||||
}
|
||||
|
||||
public async postEntity(domain: string, entity: Entity): Promise<Response> {
|
||||
const url = new URL("/.versia/v0.6/inbox", `https://${domain}`);
|
||||
|
||||
const req = new Request(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Accept: CONTENT_TYPE,
|
||||
"User-Agent": DEFAULT_UA,
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Content-Type": "application/vnd.versia+json; charset=utf-8",
|
||||
},
|
||||
body: JSON.stringify(entity.toJSON()),
|
||||
});
|
||||
|
||||
const finalReq = await sign(this.privateKey, this.authorUrl, req);
|
||||
const finalReq = await sign(this.privateKey, this.instance, req);
|
||||
|
||||
return fetch(finalReq);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively go through a Collection of entities until reaching the end
|
||||
* @param url URL to reach the Collection
|
||||
* @param expectedType
|
||||
* @param reference Entity Reference
|
||||
* @param entityType
|
||||
* @param collectionItemType
|
||||
* @param options.limit Limit the number of entities to fetch
|
||||
*/
|
||||
public async resolveCollection<T extends typeof Entity>(
|
||||
url: URL,
|
||||
expectedType: T,
|
||||
public async resolveCollection<
|
||||
E extends typeof Entity,
|
||||
T extends typeof Entity,
|
||||
>(
|
||||
reference: Reference,
|
||||
collectionName: string,
|
||||
entityType: E,
|
||||
collectionItemType: T,
|
||||
options?: {
|
||||
limit?: number;
|
||||
},
|
||||
): Promise<InstanceType<T>[]> {
|
||||
const url = new URL(
|
||||
`/.versia/v0.6/entities/${encodeURIComponent(
|
||||
entityType.name,
|
||||
)}/${encodeURIComponent(reference.id)}/collections/${encodeURIComponent(
|
||||
collectionName,
|
||||
)}`,
|
||||
`https://${reference.domain}`,
|
||||
);
|
||||
|
||||
const entities: InstanceType<T>[] = [];
|
||||
let nextUrl: URL | null = url;
|
||||
let limit = options?.limit ?? Number.POSITIVE_INFINITY;
|
||||
|
||||
while (nextUrl && limit > 0) {
|
||||
const collection: Collection = await this.fetchEntity(
|
||||
nextUrl,
|
||||
Collection,
|
||||
);
|
||||
let collection = await this.fetchSigned(url, Collection);
|
||||
const total = collection.data.total;
|
||||
|
||||
for (const entity of collection.data.items) {
|
||||
if (entity.type === expectedType.name) {
|
||||
entities.push(
|
||||
(await expectedType.fromJSON(
|
||||
entity,
|
||||
)) as InstanceType<T>,
|
||||
);
|
||||
}
|
||||
while (collection && limit > 0) {
|
||||
entities.push(
|
||||
...collection.data.items.map(
|
||||
(item) =>
|
||||
collectionItemType.fromJSON(item) as InstanceType<T>,
|
||||
),
|
||||
);
|
||||
limit -= collection.data.items.length;
|
||||
|
||||
if (entities.length >= total) {
|
||||
break;
|
||||
}
|
||||
|
||||
nextUrl = collection.data.next
|
||||
? new URL(collection.data.next)
|
||||
: null;
|
||||
limit -= collection.data.items.length;
|
||||
url.searchParams.set("offset", entities.length.toString());
|
||||
collection = await this.fetchSigned(url, Collection);
|
||||
}
|
||||
|
||||
return entities;
|
||||
|
|
@ -129,33 +168,46 @@ export class FederationRequester {
|
|||
|
||||
/**
|
||||
* Recursively go through a URICollection of entities until reaching the end
|
||||
* @param url URL to reach the Collection
|
||||
* @param reference Entity Reference
|
||||
* @param entityType
|
||||
* @param options.limit Limit the number of entities to fetch
|
||||
*/
|
||||
public async resolveURICollection(
|
||||
url: URL,
|
||||
public async resolveURICollection<E extends typeof Entity>(
|
||||
reference: Reference,
|
||||
collectionName: string,
|
||||
entityType: E,
|
||||
options?: {
|
||||
limit?: number;
|
||||
},
|
||||
): Promise<URL[]> {
|
||||
const entities: string[] = [];
|
||||
let nextUrl: URL | null = url;
|
||||
): Promise<string[]> {
|
||||
const url = new URL(
|
||||
`/.versia/v0.6/entities/${encodeURIComponent(
|
||||
entityType.name,
|
||||
)}/${encodeURIComponent(reference.id)}/collections/${encodeURIComponent(
|
||||
collectionName,
|
||||
)}`,
|
||||
`https://${reference.domain}`,
|
||||
);
|
||||
|
||||
const uris: string[] = [];
|
||||
let limit = options?.limit ?? Number.POSITIVE_INFINITY;
|
||||
|
||||
while (nextUrl && limit > 0) {
|
||||
const collection: URICollection = await this.fetchEntity(
|
||||
nextUrl,
|
||||
URICollection,
|
||||
);
|
||||
let collection = await this.fetchSigned(url, URICollection);
|
||||
const total = collection.data.total;
|
||||
|
||||
entities.push(...collection.data.items);
|
||||
nextUrl = collection.data.next
|
||||
? new URL(collection.data.next)
|
||||
: null;
|
||||
while (collection && limit > 0) {
|
||||
uris.push(...collection.data.items);
|
||||
limit -= collection.data.items.length;
|
||||
|
||||
if (uris.length >= total) {
|
||||
break;
|
||||
}
|
||||
|
||||
url.searchParams.set("offset", uris.length.toString());
|
||||
collection = await this.fetchSigned(url, URICollection);
|
||||
}
|
||||
|
||||
return entities.map((u) => new URL(u));
|
||||
return uris;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -164,21 +216,21 @@ export class FederationRequester {
|
|||
*/
|
||||
public static async resolveWebFinger(
|
||||
username: string,
|
||||
hostname: string,
|
||||
contentType = "application/json",
|
||||
serverUrl = `https://${hostname}`,
|
||||
domain: string,
|
||||
contentType = "application/vnd.versia+json",
|
||||
serverUrl = `https://${domain}`,
|
||||
): Promise<URL | null> {
|
||||
const res = await fetch(
|
||||
new URL(
|
||||
`/.well-known/webfinger?${new URLSearchParams({
|
||||
resource: `acct:${username}@${hostname}`,
|
||||
resource: `acct:${username}@${domain}`,
|
||||
})}`,
|
||||
serverUrl,
|
||||
),
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Accept: "application/jrd+json, application/json",
|
||||
"User-Agent": DEFAULT_UA,
|
||||
},
|
||||
},
|
||||
|
|
@ -204,4 +256,57 @@ export class FederationRequester {
|
|||
|
||||
return new URL(selfLink.href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve instance metadata from a domain
|
||||
*
|
||||
* Fetches well-known for version discovery, and if versia is supported, fetches the instance metadata
|
||||
* @param domain
|
||||
*/
|
||||
public async resolveInstance(domain: string): Promise<InstanceMetadata> {
|
||||
const wellKnownUrl = new URL(
|
||||
"/.well-known/versia",
|
||||
`https://${domain}`,
|
||||
);
|
||||
|
||||
const wellKnownRes = await fetch(wellKnownUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": DEFAULT_UA,
|
||||
},
|
||||
});
|
||||
|
||||
if (!wellKnownRes.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch well-known from ${wellKnownUrl.toString()}: got HTTP code ${wellKnownRes.status} with body "${await wellKnownRes.text()}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const wellKnownData = await wellKnownRes.json();
|
||||
|
||||
if (
|
||||
!(
|
||||
wellKnownData.versions &&
|
||||
Array.isArray(wellKnownData.versions) &&
|
||||
wellKnownData.versions.includes("0.6.0")
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Instance at ${domain} does not support Versia v0.6`,
|
||||
);
|
||||
}
|
||||
|
||||
const metadataUrl = new URL(
|
||||
"/.versia/v0.6/instance",
|
||||
`https://${domain}`,
|
||||
);
|
||||
|
||||
const metadataRes = await this.fetchSigned(
|
||||
metadataUrl,
|
||||
InstanceMetadata,
|
||||
);
|
||||
|
||||
return metadataRes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
import { z } from "zod";
|
||||
import { u64, url } from "./common.ts";
|
||||
import { u64 } from "./common.ts";
|
||||
import { ReferenceSchema } from "./entity.ts";
|
||||
|
||||
export const CollectionSchema = z.strictObject({
|
||||
author: url.nullable(),
|
||||
first: url,
|
||||
last: url,
|
||||
author: ReferenceSchema.nullable(),
|
||||
total: u64,
|
||||
next: url.nullable(),
|
||||
previous: url.nullable(),
|
||||
items: z.array(z.any()),
|
||||
});
|
||||
|
||||
export const URICollectionSchema = CollectionSchema.extend({
|
||||
items: z.array(url),
|
||||
items: z.array(ReferenceSchema),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,26 +2,6 @@ import { types } from "mime-types";
|
|||
import { z } from "zod";
|
||||
import { f64, u64 } from "./common.ts";
|
||||
|
||||
const hashSizes = {
|
||||
sha256: 64,
|
||||
sha512: 128,
|
||||
"sha3-256": 64,
|
||||
"sha3-512": 128,
|
||||
"blake2b-256": 64,
|
||||
"blake2b-512": 128,
|
||||
"blake3-256": 64,
|
||||
"blake3-512": 128,
|
||||
md5: 32,
|
||||
sha1: 40,
|
||||
sha224: 56,
|
||||
sha384: 96,
|
||||
"sha3-224": 56,
|
||||
"sha3-384": 96,
|
||||
"blake2s-256": 64,
|
||||
"blake2s-512": 128,
|
||||
"blake3-224": 56,
|
||||
"blake3-384": 96,
|
||||
};
|
||||
const allMimeTypes = Object.values(types) as [string, ...string[]];
|
||||
const textMimeTypes = Object.values(types).filter((v) =>
|
||||
v.startsWith("text/"),
|
||||
|
|
@ -46,16 +26,7 @@ export const ContentFormatSchema = z.partialRecord(
|
|||
remote: z.boolean(),
|
||||
description: z.string().nullish(),
|
||||
size: u64.nullish(),
|
||||
hash: z
|
||||
.strictObject(
|
||||
Object.fromEntries(
|
||||
Object.entries(hashSizes).map(([k, v]) => [
|
||||
k,
|
||||
z.string().length(v).nullish(),
|
||||
]),
|
||||
),
|
||||
)
|
||||
.nullish(),
|
||||
hash: z.hash("sha256").nullish(),
|
||||
thumbhash: z.string().nullish(),
|
||||
width: u64.nullish(),
|
||||
height: u64.nullish(),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "./common.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
import { ReferenceSchema, TransientEntitySchema } from "./entity.ts";
|
||||
|
||||
export const DeleteSchema = EntitySchema.extend({
|
||||
uri: z.null().optional(),
|
||||
export const DeleteSchema = TransientEntitySchema.extend({
|
||||
type: z.literal("Delete"),
|
||||
author: url.nullable(),
|
||||
author: ReferenceSchema,
|
||||
deleted_type: z.string(),
|
||||
deleted: url,
|
||||
deleted: ReferenceSchema,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { z } from "zod";
|
||||
import { isISOString } from "../regex.ts";
|
||||
import { url } from "./common.ts";
|
||||
import { CustomEmojiExtensionSchema } from "./extensions/emojis.ts";
|
||||
|
||||
export const ExtensionPropertySchema = z
|
||||
|
|
@ -10,14 +9,26 @@ export const ExtensionPropertySchema = z
|
|||
})
|
||||
.catchall(z.any());
|
||||
|
||||
export const ReferenceSchema = z.string();
|
||||
|
||||
export const EntitySchema = z.strictObject({
|
||||
// biome-ignore lint/style/useNamingConvention: required for JSON schema
|
||||
$schema: z.url().nullish(),
|
||||
id: z.string().max(512),
|
||||
id: z
|
||||
.string()
|
||||
.max(512)
|
||||
.regex(
|
||||
// a-z, A-Z, 0-9, - and _
|
||||
/^[A-Za-z0-9\-_]+$/,
|
||||
"can only contain alphanumeric characters, hyphens and underscores",
|
||||
),
|
||||
created_at: z
|
||||
.string()
|
||||
.refine((v) => isISOString(v), "must be a valid ISO8601 datetime"),
|
||||
uri: url,
|
||||
.refine((v) => isISOString(v), "must be a valid RFC 3339 datetime"),
|
||||
type: z.string(),
|
||||
extensions: ExtensionPropertySchema.nullish(),
|
||||
});
|
||||
|
||||
export const TransientEntitySchema = EntitySchema.extend({
|
||||
id: z.null().optional(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,41 +1,38 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { TextContentFormatSchema } from "../contentformat.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
import {
|
||||
EntitySchema,
|
||||
ReferenceSchema,
|
||||
TransientEntitySchema,
|
||||
} from "../entity.ts";
|
||||
|
||||
export const GroupSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/Group"),
|
||||
name: TextContentFormatSchema.nullish(),
|
||||
description: TextContentFormatSchema.nullish(),
|
||||
open: z.boolean().nullish(),
|
||||
members: url,
|
||||
notes: url.nullish(),
|
||||
open: z.boolean(),
|
||||
});
|
||||
|
||||
export const GroupSubscribeSchema = EntitySchema.extend({
|
||||
export const GroupSubscribeSchema = TransientEntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/Subscribe"),
|
||||
uri: z.null().optional(),
|
||||
subscriber: url,
|
||||
group: url,
|
||||
subscriber: ReferenceSchema,
|
||||
group: ReferenceSchema,
|
||||
});
|
||||
|
||||
export const GroupUnsubscribeSchema = EntitySchema.extend({
|
||||
export const GroupUnsubscribeSchema = TransientEntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/Unsubscribe"),
|
||||
uri: z.null().optional(),
|
||||
subscriber: url,
|
||||
group: url,
|
||||
subscriber: ReferenceSchema,
|
||||
group: ReferenceSchema,
|
||||
});
|
||||
|
||||
export const GroupSubscribeAcceptSchema = EntitySchema.extend({
|
||||
export const GroupSubscribeAcceptSchema = TransientEntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/SubscribeAccept"),
|
||||
uri: z.null().optional(),
|
||||
subscriber: url,
|
||||
group: url,
|
||||
subscriber: ReferenceSchema,
|
||||
group: ReferenceSchema,
|
||||
});
|
||||
|
||||
export const GroupSubscribeRejectSchema = EntitySchema.extend({
|
||||
export const GroupSubscribeRejectSchema = TransientEntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/SubscribeReject"),
|
||||
uri: z.null().optional(),
|
||||
subscriber: url,
|
||||
group: url,
|
||||
subscriber: ReferenceSchema,
|
||||
group: ReferenceSchema,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
import { EntitySchema, ReferenceSchema } from "../entity.ts";
|
||||
|
||||
export const LikeSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:likes/Like"),
|
||||
author: url,
|
||||
liked: url,
|
||||
author: ReferenceSchema,
|
||||
liked: ReferenceSchema,
|
||||
});
|
||||
|
||||
export const DislikeSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:likes/Dislike"),
|
||||
author: url,
|
||||
disliked: url,
|
||||
author: ReferenceSchema,
|
||||
disliked: ReferenceSchema,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
import { ReferenceSchema, TransientEntitySchema } from "../entity.ts";
|
||||
|
||||
export const MigrationSchema = EntitySchema.extend({
|
||||
export const MigrationSchema = TransientEntitySchema.extend({
|
||||
type: z.literal("pub.versia:migration/Migration"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
destination: url,
|
||||
author: ReferenceSchema,
|
||||
destination: ReferenceSchema,
|
||||
});
|
||||
|
||||
export const MigrationExtensionSchema = z.strictObject({
|
||||
previous: url,
|
||||
new: url.nullish(),
|
||||
previous: ReferenceSchema,
|
||||
new: ReferenceSchema.nullish(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { z } from "zod";
|
||||
import { isISOString } from "../../regex.ts";
|
||||
import { u64, url } from "../common.ts";
|
||||
import { u64 } from "../common.ts";
|
||||
import { TextContentFormatSchema } from "../contentformat.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
import { EntitySchema, ReferenceSchema } from "../entity.ts";
|
||||
|
||||
export const VoteSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:polls/Vote"),
|
||||
author: url,
|
||||
poll: url,
|
||||
author: ReferenceSchema,
|
||||
poll: ReferenceSchema,
|
||||
option: u64,
|
||||
});
|
||||
|
||||
|
|
@ -17,6 +17,6 @@ export const PollExtensionSchema = z.strictObject({
|
|||
multiple_choice: z.boolean(),
|
||||
expires_at: z
|
||||
.string()
|
||||
.refine((v) => isISOString(v), "must be a valid ISO8601 datetime")
|
||||
.refine((v) => isISOString(v), "must be a valid RFC 3339 datetime")
|
||||
.nullish(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
import { EntitySchema, ReferenceSchema } from "../entity.ts";
|
||||
|
||||
export const ReactionSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:reactions/Reaction"),
|
||||
author: url,
|
||||
object: url,
|
||||
author: ReferenceSchema,
|
||||
object: ReferenceSchema,
|
||||
content: z.string().min(1).max(256),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
import { ReferenceSchema, TransientEntitySchema } from "../entity.ts";
|
||||
|
||||
export const ReportSchema = EntitySchema.extend({
|
||||
export const ReportSchema = TransientEntitySchema.extend({
|
||||
type: z.literal("pub.versia:reports/Report"),
|
||||
uri: z.null().optional(),
|
||||
author: url.nullish(),
|
||||
reported: z.array(url),
|
||||
author: ReferenceSchema.nullish(),
|
||||
reported: z.array(ReferenceSchema),
|
||||
tags: z.array(z.string()),
|
||||
comment: z
|
||||
.string()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
import { EntitySchema, ReferenceSchema } from "../entity.ts";
|
||||
|
||||
export const ShareSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:share/Share"),
|
||||
author: url,
|
||||
shared: url,
|
||||
author: ReferenceSchema,
|
||||
shared: ReferenceSchema,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@
|
|||
|
||||
import { z } from "zod";
|
||||
import { ianaTimezoneRegex, isISOString } from "../../regex.ts";
|
||||
import { url } from "../common.ts";
|
||||
import {
|
||||
AudioContentFormatSchema,
|
||||
ImageContentFormatSchema,
|
||||
} from "../contentformat.ts";
|
||||
import { ReferenceSchema } from "../entity.ts";
|
||||
|
||||
export const VanityExtensionSchema = z.strictObject({
|
||||
avatar_overlays: z.array(ImageContentFormatSchema).nullish(),
|
||||
|
|
@ -21,24 +21,21 @@ export const VanityExtensionSchema = z.strictObject({
|
|||
pronouns: z.record(
|
||||
z.string(),
|
||||
z.array(
|
||||
z.union([
|
||||
z.strictObject({
|
||||
subject: z.string(),
|
||||
object: z.string(),
|
||||
dependent_possessive: z.string(),
|
||||
independent_possessive: z.string(),
|
||||
reflexive: z.string(),
|
||||
}),
|
||||
z.string(),
|
||||
]),
|
||||
z.strictObject({
|
||||
subject: z.string(),
|
||||
object: z.string(),
|
||||
dependent_possessive: z.string(),
|
||||
independent_possessive: z.string(),
|
||||
reflexive: z.string(),
|
||||
}),
|
||||
),
|
||||
),
|
||||
birthday: z
|
||||
.string()
|
||||
.refine((v) => isISOString(v), "must be a valid ISO8601 datetime")
|
||||
.refine((v) => isISOString(v), "must be a valid RFC 3339 datetime")
|
||||
.nullish(),
|
||||
location: z.string().nullish(),
|
||||
aliases: z.array(url).nullish(),
|
||||
aliases: z.array(ReferenceSchema).nullish(),
|
||||
timezone: z
|
||||
.string()
|
||||
.regex(ianaTimezoneRegex, "must be a valid IANA timezone")
|
||||
|
|
|
|||
|
|
@ -1,31 +1,26 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "./common.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
import { ReferenceSchema, TransientEntitySchema } from "./entity.ts";
|
||||
|
||||
export const FollowSchema = EntitySchema.extend({
|
||||
export const FollowSchema = TransientEntitySchema.extend({
|
||||
type: z.literal("Follow"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
followee: url,
|
||||
author: ReferenceSchema,
|
||||
followee: ReferenceSchema,
|
||||
});
|
||||
|
||||
export const FollowAcceptSchema = EntitySchema.extend({
|
||||
export const FollowAcceptSchema = TransientEntitySchema.extend({
|
||||
type: z.literal("FollowAccept"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
follower: url,
|
||||
author: ReferenceSchema,
|
||||
follower: ReferenceSchema,
|
||||
});
|
||||
|
||||
export const FollowRejectSchema = EntitySchema.extend({
|
||||
export const FollowRejectSchema = TransientEntitySchema.extend({
|
||||
type: z.literal("FollowReject"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
follower: url,
|
||||
author: ReferenceSchema,
|
||||
follower: ReferenceSchema,
|
||||
});
|
||||
|
||||
export const UnfollowSchema = EntitySchema.extend({
|
||||
export const UnfollowSchema = TransientEntitySchema.extend({
|
||||
type: z.literal("Unfollow"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
followee: url,
|
||||
author: ReferenceSchema,
|
||||
followee: ReferenceSchema,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ export {
|
|||
VideoContentFormatSchema,
|
||||
} from "./contentformat.ts";
|
||||
export { DeleteSchema } from "./delete.ts";
|
||||
export { EntitySchema } from "./entity.ts";
|
||||
export {
|
||||
EntitySchema,
|
||||
ReferenceSchema,
|
||||
TransientEntitySchema,
|
||||
} from "./entity.ts";
|
||||
export { DislikeSchema, LikeSchema } from "./extensions/likes.ts";
|
||||
export { VoteSchema } from "./extensions/polls.ts";
|
||||
export { ReactionSchema } from "./extensions/reactions.ts";
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
import { z } from "zod";
|
||||
import { extensionRegex, semverRegex } from "../regex.ts";
|
||||
import { url } from "./common.ts";
|
||||
import { ImageContentFormatSchema } from "./contentformat.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
import { TransientEntitySchema } from "./entity.ts";
|
||||
|
||||
export const InstanceMetadataSchema = EntitySchema.extend({
|
||||
export const InstanceMetadataSchema = TransientEntitySchema.extend({
|
||||
type: z.literal("InstanceMetadata"),
|
||||
id: z.null().optional(),
|
||||
uri: z.null().optional(),
|
||||
name: z.string().min(1),
|
||||
software: z.strictObject({
|
||||
name: z.string().min(1),
|
||||
|
|
@ -28,14 +25,11 @@ export const InstanceMetadataSchema = EntitySchema.extend({
|
|||
),
|
||||
}),
|
||||
description: z.string().nullish(),
|
||||
host: z.string(),
|
||||
shared_inbox: url.nullish(),
|
||||
domain: z.string(),
|
||||
public_key: z.strictObject({
|
||||
key: z.string().min(1),
|
||||
algorithm: z.literal("ed25519"),
|
||||
}),
|
||||
moderators: url.nullish(),
|
||||
admins: url.nullish(),
|
||||
logo: ImageContentFormatSchema.nullish(),
|
||||
banner: ImageContentFormatSchema.nullish(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import {
|
|||
NonTextContentFormatSchema,
|
||||
TextContentFormatSchema,
|
||||
} from "./contentformat.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
import { EntitySchema, ReferenceSchema } from "./entity.ts";
|
||||
import { PollExtensionSchema } from "./extensions/polls.ts";
|
||||
|
||||
export const NoteSchema = EntitySchema.extend({
|
||||
type: z.literal("Note"),
|
||||
attachments: z.array(NonTextContentFormatSchema).nullish(),
|
||||
author: url,
|
||||
attachments: z.array(NonTextContentFormatSchema),
|
||||
author: ReferenceSchema,
|
||||
category: z
|
||||
.enum([
|
||||
"microblog",
|
||||
|
|
@ -23,16 +23,6 @@ export const NoteSchema = EntitySchema.extend({
|
|||
])
|
||||
.nullish(),
|
||||
content: TextContentFormatSchema.nullish(),
|
||||
collections: z
|
||||
.strictObject({
|
||||
replies: url,
|
||||
quotes: url,
|
||||
"pub.versia:reactions/Reactions": url.nullish(),
|
||||
"pub.versia:share/Shares": url.nullish(),
|
||||
"pub.versia:likes/Likes": url.nullish(),
|
||||
"pub.versia:likes/Dislikes": url.nullish(),
|
||||
})
|
||||
.catchall(url),
|
||||
device: z
|
||||
.strictObject({
|
||||
name: z.string(),
|
||||
|
|
@ -40,22 +30,20 @@ export const NoteSchema = EntitySchema.extend({
|
|||
url: url.nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
group: url.or(z.enum(["public", "followers"])).nullish(),
|
||||
is_sensitive: z.boolean().nullish(),
|
||||
mentions: z.array(url).nullish(),
|
||||
previews: z
|
||||
.array(
|
||||
z.strictObject({
|
||||
link: url,
|
||||
title: z.string(),
|
||||
description: z.string().nullish(),
|
||||
image: url.nullish(),
|
||||
icon: url.nullish(),
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
quotes: url.nullish(),
|
||||
replies_to: url.nullish(),
|
||||
group: ReferenceSchema.or(z.enum(["public", "followers"])).nullish(),
|
||||
is_sensitive: z.boolean(),
|
||||
mentions: z.array(ReferenceSchema),
|
||||
previews: z.array(
|
||||
z.strictObject({
|
||||
link: url,
|
||||
title: z.string(),
|
||||
description: z.string().nullish(),
|
||||
image: url.nullish(),
|
||||
icon: url.nullish(),
|
||||
}),
|
||||
),
|
||||
quotes: ReferenceSchema.nullish(),
|
||||
replies_to: ReferenceSchema.nullish(),
|
||||
subject: z.string().nullish(),
|
||||
extensions: EntitySchema.shape.extensions
|
||||
.unwrap()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "./common.ts";
|
||||
import {
|
||||
ImageContentFormatSchema,
|
||||
TextContentFormatSchema,
|
||||
|
|
@ -8,25 +7,17 @@ import { EntitySchema } from "./entity.ts";
|
|||
import { MigrationExtensionSchema } from "./extensions/migration.ts";
|
||||
import { VanityExtensionSchema } from "./extensions/vanity.ts";
|
||||
|
||||
export const PublicKeyDataSchema = z.strictObject({
|
||||
key: z.string().min(1),
|
||||
actor: url,
|
||||
algorithm: z.literal("ed25519"),
|
||||
});
|
||||
|
||||
export const UserSchema = EntitySchema.extend({
|
||||
type: z.literal("User"),
|
||||
avatar: ImageContentFormatSchema.nullish(),
|
||||
bio: TextContentFormatSchema.nullish(),
|
||||
display_name: z.string().nullish(),
|
||||
fields: z
|
||||
.array(
|
||||
z.strictObject({
|
||||
key: TextContentFormatSchema,
|
||||
value: TextContentFormatSchema,
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
fields: z.array(
|
||||
z.strictObject({
|
||||
key: TextContentFormatSchema,
|
||||
value: TextContentFormatSchema,
|
||||
}),
|
||||
),
|
||||
username: z
|
||||
.string()
|
||||
.min(1)
|
||||
|
|
@ -35,20 +26,8 @@ export const UserSchema = EntitySchema.extend({
|
|||
"must be alphanumeric, and may contain _ or -",
|
||||
),
|
||||
header: ImageContentFormatSchema.nullish(),
|
||||
public_key: PublicKeyDataSchema,
|
||||
manually_approves_followers: z.boolean().nullish(),
|
||||
indexable: z.boolean().nullish(),
|
||||
inbox: url,
|
||||
collections: z
|
||||
.object({
|
||||
featured: url,
|
||||
followers: url,
|
||||
following: url,
|
||||
outbox: url,
|
||||
"pub.versia:likes/Likes": url.nullish(),
|
||||
"pub.versia:likes/Dislikes": url.nullish(),
|
||||
})
|
||||
.catchall(url),
|
||||
manually_approves_followers: z.boolean(),
|
||||
indexable: z.boolean(),
|
||||
extensions: EntitySchema.shape.extensions
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue