feat(api): 🏷️ Port Status OpenAPI schemas from Mastodon API docs

This commit is contained in:
Jesse Wierzbinski 2025-02-05 22:49:07 +01:00
parent 2aeada4904
commit 7c622730dc
No known key found for this signature in database
26 changed files with 920 additions and 148 deletions

View file

@ -1,8 +1,9 @@
import { apiRoute, auth, withUserParam } from "@/api"; import { apiRoute, auth, withUserParam } from "@/api";
import { createRoute, z } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { Note, Timeline } from "@versia/kit/db"; import { Timeline } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables"; import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm"; import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
import { Status } from "~/classes/schemas/status";
const schemas = { const schemas = {
param: z.object({ param: z.object({
@ -61,7 +62,7 @@ const route = createRoute({
description: "A list of statuses by the specified account", description: "A list of statuses by the specified account",
content: { content: {
"application/json": { "application/json": {
schema: z.array(Note.schema), schema: z.array(Status),
}, },
}, },
headers: { headers: {

View file

@ -3,6 +3,7 @@ import { createRoute } from "@hono/zod-openapi";
import { Application } from "@versia/kit/db"; import { Application } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { ApiError } from "~/classes/errors/api-error"; import { ApiError } from "~/classes/errors/api-error";
import { Application as ApplicationSchema } from "~/classes/schemas/application";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
const route = createRoute({ const route = createRoute({
@ -21,7 +22,7 @@ const route = createRoute({
description: "Application", description: "Application",
content: { content: {
"application/json": { "application/json": {
schema: Application.schema, schema: ApplicationSchema,
}, },
}, },
}, },
@ -52,13 +53,6 @@ export default apiRoute((app) =>
throw new ApiError(401, "Application not found"); throw new ApiError(401, "Application not found");
} }
return context.json( return context.json(application.toApi(), 200);
{
...application.toApi(),
redirect_uris: application.data.redirectUri,
scopes: application.data.scopes,
},
200,
);
}), }),
); );

View file

@ -1,8 +1,9 @@
import { apiRoute, auth } from "@/api"; import { apiRoute, auth } from "@/api";
import { createRoute, z } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { Note, Timeline } from "@versia/kit/db"; import { Timeline } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables"; import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm"; import { and, gt, gte, lt, sql } from "drizzle-orm";
import { Status } from "~/classes/schemas/status";
const schemas = { const schemas = {
query: z.object({ query: z.object({
@ -31,7 +32,7 @@ const route = createRoute({
description: "Favourites", description: "Favourites",
content: { content: {
"application/json": { "application/json": {
schema: z.array(Note.schema), schema: z.array(Status),
}, },
}, },
}, },

View file

@ -1,7 +1,7 @@
import { apiRoute, auth, withNoteParam } from "@/api"; import { apiRoute, auth, withNoteParam } from "@/api";
import { createRoute, z } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { Status } from "~/classes/schemas/status";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
const route = createRoute({ const route = createRoute({
@ -26,8 +26,8 @@ const route = createRoute({
content: { content: {
"application/json": { "application/json": {
schema: z.object({ schema: z.object({
ancestors: z.array(Note.schema), ancestors: z.array(Status),
descendants: z.array(Note.schema), descendants: z.array(Status),
}), }),
}, },
}, },

View file

@ -1,7 +1,7 @@
import { apiRoute, auth, withNoteParam } from "@/api"; import { apiRoute, auth, withNoteParam } from "@/api";
import { createRoute, z } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { Status } from "~/classes/schemas/status";
const route = createRoute({ const route = createRoute({
method: "post", method: "post",
@ -27,7 +27,7 @@ const route = createRoute({
description: "Favourited status", description: "Favourited status",
content: { content: {
"application/json": { "application/json": {
schema: Note.schema, schema: Status,
}, },
}, },
}, },

View file

@ -1,9 +1,10 @@
import { apiRoute, auth, jsonOrForm, withNoteParam } from "@/api"; import { apiRoute, auth, jsonOrForm, withNoteParam } from "@/api";
import { createRoute, z } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { Media, Note } from "@versia/kit/db"; import { Media } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { ApiError } from "~/classes/errors/api-error"; import { ApiError } from "~/classes/errors/api-error";
import { Status } from "~/classes/schemas/status";
import { config } from "~/packages/config-manager/index.ts"; import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -84,7 +85,7 @@ const routeGet = createRoute({
description: "Status", description: "Status",
content: { content: {
"application/json": { "application/json": {
schema: Note.schema, schema: Status,
}, },
}, },
}, },
@ -121,7 +122,7 @@ const routeDelete = createRoute({
description: "Deleted status", description: "Deleted status",
content: { content: {
"application/json": { "application/json": {
schema: Note.schema, schema: Status,
}, },
}, },
}, },
@ -180,7 +181,7 @@ const routePut = createRoute({
description: "Updated status", description: "Updated status",
content: { content: {
"application/json": { "application/json": {
schema: Note.schema, schema: Status,
}, },
}, },
}, },

View file

@ -1,9 +1,10 @@
import { apiRoute, auth, withNoteParam } from "@/api"; import { apiRoute, auth, withNoteParam } from "@/api";
import { createRoute, z } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { Note, db } from "@versia/kit/db"; import { db } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import type { SQL } from "drizzle-orm"; import type { SQL } from "drizzle-orm";
import { ApiError } from "~/classes/errors/api-error"; import { ApiError } from "~/classes/errors/api-error";
import { Status } from "~/classes/schemas/status";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
const route = createRoute({ const route = createRoute({
@ -30,7 +31,7 @@ const route = createRoute({
description: "Pinned status", description: "Pinned status",
content: { content: {
"application/json": { "application/json": {
schema: Note.schema, schema: Status,
}, },
}, },
}, },

View file

@ -4,6 +4,7 @@ import { Note } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables"; import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { ApiError } from "~/classes/errors/api-error"; import { ApiError } from "~/classes/errors/api-error";
import { Status } from "~/classes/schemas/status";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
const schemas = { const schemas = {
@ -51,7 +52,7 @@ const route = createRoute({
description: "Reblogged status", description: "Reblogged status",
content: { content: {
"application/json": { "application/json": {
schema: Note.schema, schema: Status,
}, },
}, },
}, },

View file

@ -1,7 +1,7 @@
import { apiRoute, auth, withNoteParam } from "@/api"; import { apiRoute, auth, withNoteParam } from "@/api";
import { createRoute, z } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { Status } from "~/classes/schemas/status";
const route = createRoute({ const route = createRoute({
method: "post", method: "post",
@ -27,7 +27,7 @@ const route = createRoute({
description: "Unfavourited status", description: "Unfavourited status",
content: { content: {
"application/json": { "application/json": {
schema: Note.schema, schema: Status,
}, },
}, },
}, },

View file

@ -1,8 +1,8 @@
import { apiRoute, auth, withNoteParam } from "@/api"; import { apiRoute, auth, withNoteParam } from "@/api";
import { createRoute, z } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import { ApiError } from "~/classes/errors/api-error"; import { ApiError } from "~/classes/errors/api-error";
import { Status } from "~/classes/schemas/status";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
const route = createRoute({ const route = createRoute({
@ -29,7 +29,7 @@ const route = createRoute({
description: "Unpinned status", description: "Unpinned status",
content: { content: {
"application/json": { "application/json": {
schema: Note.schema, schema: Status,
}, },
}, },
}, },

View file

@ -4,6 +4,7 @@ import { Note } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables"; import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { ApiError } from "~/classes/errors/api-error"; import { ApiError } from "~/classes/errors/api-error";
import { Status } from "~/classes/schemas/status";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
const route = createRoute({ const route = createRoute({
@ -30,7 +31,7 @@ const route = createRoute({
description: "Unreblogged status", description: "Unreblogged status",
content: { content: {
"application/json": { "application/json": {
schema: Note.schema, schema: Status,
}, },
}, },
}, },

View file

@ -4,6 +4,7 @@ import { Media, Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { ApiError } from "~/classes/errors/api-error"; import { ApiError } from "~/classes/errors/api-error";
import { Status } from "~/classes/schemas/status";
import { config } from "~/packages/config-manager/index.ts"; import { config } from "~/packages/config-manager/index.ts";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -116,7 +117,7 @@ const route = createRoute({
description: "The new status", description: "The new status",
content: { content: {
"application/json": { "application/json": {
schema: Note.schema, schema: Status,
}, },
}, },
}, },

View file

@ -1,8 +1,9 @@
import { apiRoute, auth } from "@/api"; import { apiRoute, auth } from "@/api";
import { createRoute, z } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { Note, Timeline } from "@versia/kit/db"; import { Timeline } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables"; import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm"; import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm";
import { Status } from "~/classes/schemas/status";
const schemas = { const schemas = {
query: z.object({ query: z.object({
@ -36,7 +37,7 @@ const route = createRoute({
description: "Home timeline", description: "Home timeline",
content: { content: {
"application/json": { "application/json": {
schema: z.array(Note.schema), schema: z.array(Status),
}, },
}, },
}, },

View file

@ -1,8 +1,9 @@
import { apiRoute, auth } from "@/api"; import { apiRoute, auth } from "@/api";
import { createRoute, z } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { Note, Timeline } from "@versia/kit/db"; import { Timeline } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables"; import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm"; import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm";
import { Status } from "~/classes/schemas/status";
const schemas = { const schemas = {
query: z.object({ query: z.object({
@ -47,7 +48,7 @@ const route = createRoute({
description: "Public timeline", description: "Public timeline",
content: { content: {
"application/json": { "application/json": {
schema: z.array(Note.schema), schema: z.array(Status),
}, },
}, },
}, },

View file

@ -5,6 +5,7 @@ import { Instances, Notes, RolePermissions, Users } from "@versia/kit/tables";
import { and, eq, inArray, isNull, sql } from "drizzle-orm"; import { and, eq, inArray, isNull, sql } from "drizzle-orm";
import { ApiError } from "~/classes/errors/api-error"; import { ApiError } from "~/classes/errors/api-error";
import { Account } from "~/classes/schemas/account"; import { Account } from "~/classes/schemas/account";
import { Status } from "~/classes/schemas/status";
import { searchManager } from "~/classes/search/search-manager"; import { searchManager } from "~/classes/search/search-manager";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -48,7 +49,7 @@ const route = createRoute({
"application/json": { "application/json": {
schema: z.object({ schema: z.object({
accounts: z.array(Account), accounts: z.array(Account),
statuses: z.array(Note.schema), statuses: z.array(Status),
hashtags: z.array(z.string()), hashtags: z.array(z.string()),
}), }),
}, },

View file

@ -1,5 +1,4 @@
import { z } from "@hono/zod-openapi"; import type { z } from "@hono/zod-openapi";
import type { Application as APIApplication } from "@versia/client/types";
import { Token, db } from "@versia/kit/db"; import { Token, db } from "@versia/kit/db";
import { Applications } from "@versia/kit/tables"; import { Applications } from "@versia/kit/tables";
import { import {
@ -10,19 +9,15 @@ import {
eq, eq,
inArray, inArray,
} from "drizzle-orm"; } from "drizzle-orm";
import type {
Application as ApplicationSchema,
CredentialApplication,
} from "../schemas/application.ts";
import { BaseInterface } from "./base.ts"; import { BaseInterface } from "./base.ts";
type ApplicationType = InferSelectModel<typeof Applications>; type ApplicationType = InferSelectModel<typeof Applications>;
export class Application extends BaseInterface<typeof Applications> { export class Application extends BaseInterface<typeof Applications> {
public static schema: z.ZodType<APIApplication> = z.object({
name: z.string(),
website: z.string().url().optional().nullable(),
vapid_key: z.string().optional().nullable(),
redirect_uris: z.string().optional(),
scopes: z.string().optional(),
});
public static $type: ApplicationType; public static $type: ApplicationType;
public async reload(): Promise<void> { public async reload(): Promise<void> {
@ -144,11 +139,26 @@ export class Application extends BaseInterface<typeof Applications> {
return this.data.id; return this.data.id;
} }
public toApi(): APIApplication { public toApi(): z.infer<typeof ApplicationSchema> {
return { return {
name: this.data.name, name: this.data.name,
website: this.data.website, website: this.data.website,
vapid_key: this.data.vapidKey, scopes: this.data.scopes.split(" "),
redirect_uri: this.data.redirectUri,
redirect_uris: this.data.redirectUri.split("\n"),
};
}
public toApiCredential(): z.infer<typeof CredentialApplication> {
return {
name: this.data.name,
website: this.data.website,
client_id: this.data.clientId,
client_secret: this.data.secret,
client_secret_expires_at: "0",
scopes: this.data.scopes.split(" "),
redirect_uri: this.data.redirectUri,
redirect_uris: this.data.redirectUri.split("\n"),
}; };
} }
} }

View file

@ -3,7 +3,7 @@ import { localObjectUri } from "@/constants";
import { mergeAndDeduplicate } from "@/lib.ts"; import { mergeAndDeduplicate } from "@/lib.ts";
import { sanitizedHtmlStrip } from "@/sanitization"; import { sanitizedHtmlStrip } from "@/sanitization";
import { sentry } from "@/sentry"; import { sentry } from "@/sentry";
import { z } from "@hono/zod-openapi"; import type { z } from "@hono/zod-openapi";
import { getLogger } from "@logtape/logtape"; import { getLogger } from "@logtape/logtape";
import type { import type {
Attachment as ApiAttachment, Attachment as ApiAttachment,
@ -43,7 +43,7 @@ import {
} from "~/classes/functions/status"; } from "~/classes/functions/status";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts"; import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts";
import { Account } from "../schemas/account.ts"; import type { Status } from "../schemas/status.ts";
import { Application } from "./application.ts"; import { Application } from "./application.ts";
import { BaseInterface } from "./base.ts"; import { BaseInterface } from "./base.ts";
import { Emoji } from "./emoji.ts"; import { Emoji } from "./emoji.ts";
@ -81,96 +81,6 @@ export type NoteTypeWithoutRecursiveRelations = Omit<
* Gives helpers to fetch notes from database in a nice format * Gives helpers to fetch notes from database in a nice format
*/ */
export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> { export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
public static schema: z.ZodType<ApiStatus> = z.object({
id: z.string().uuid(),
uri: z.string().url(),
url: z.string().url(),
account: Account,
in_reply_to_id: z.string().uuid().nullable(),
in_reply_to_account_id: z.string().uuid().nullable(),
reblog: z.lazy(() => Note.schema).nullable(),
content: z.string(),
plain_content: z.string().nullable(),
created_at: z.string(),
edited_at: z.string().nullable(),
emojis: z.array(Emoji.schema),
replies_count: z.number().int().nonnegative(),
reblogs_count: z.number().int().nonnegative(),
favourites_count: z.number().int().nonnegative(),
reblogged: z.boolean().nullable(),
favourited: z.boolean().nullable(),
muted: z.boolean().nullable(),
sensitive: z.boolean(),
spoiler_text: z.string(),
visibility: z.enum(["public", "unlisted", "private", "direct"]),
media_attachments: z.array(Media.schema),
mentions: z.array(
z.object({
id: z.string().uuid(),
username: z.string(),
acct: z.string(),
url: z.string().url(),
}),
),
tags: z.array(z.object({ name: z.string(), url: z.string().url() })),
card: z
.object({
url: z.string().url(),
title: z.string(),
description: z.string(),
type: z.enum(["link", "photo", "video", "rich"]),
image: z.string().url().nullable(),
author_name: z.string().nullable(),
author_url: z.string().url().nullable(),
provider_name: z.string().nullable(),
provider_url: z.string().url().nullable(),
html: z.string().nullable(),
width: z.number().int().nonnegative().nullable(),
height: z.number().int().nonnegative().nullable(),
embed_url: z.string().url().nullable(),
blurhash: z.string().nullable(),
})
.nullable(),
poll: z
.object({
id: z.string().uuid(),
expires_at: z.string(),
expired: z.boolean(),
multiple: z.boolean(),
votes_count: z.number().int().nonnegative(),
voted: z.boolean(),
options: z.array(
z.object({
title: z.string(),
votes_count: z.number().int().nonnegative().nullable(),
}),
),
})
.nullable(),
application: z
.object({
name: z.string(),
website: z.string().url().nullable().optional(),
vapid_key: z.string().nullable().optional(),
})
.nullable(),
language: z.string().nullable(),
pinned: z.boolean().nullable(),
emoji_reactions: z.array(
z.object({
count: z.number().int().nonnegative(),
me: z.boolean(),
name: z.string(),
url: z.string().url().optional(),
static_url: z.string().url().optional(),
accounts: z.array(Account).optional(),
account_ids: z.array(z.string().uuid()).optional(),
}),
),
quote: z.lazy(() => Note.schema).nullable(),
bookmarked: z.boolean(),
});
public static $type: NoteTypeWithRelations; public static $type: NoteTypeWithRelations;
public save(): Promise<NoteTypeWithRelations> { public save(): Promise<NoteTypeWithRelations> {
@ -861,7 +771,9 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
* @param userFetching - The user fetching the note (used to check if the note is favourite and such) * @param userFetching - The user fetching the note (used to check if the note is favourite and such)
* @returns The note in the Mastodon API format * @returns The note in the Mastodon API format
*/ */
public async toApi(userFetching?: User | null): Promise<ApiStatus> { public async toApi(
userFetching?: User | null,
): Promise<z.infer<typeof Status>> {
const data = this.data; const data = this.data;
// Convert mentions of local users from @username@host to @username // Convert mentions of local users from @username@host to @username
@ -893,7 +805,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
created_at: new Date(data.createdAt).toISOString(), created_at: new Date(data.createdAt).toISOString(),
application: data.application application: data.application
? new Application(data.application).toApi() ? new Application(data.application).toApi()
: null, : undefined,
card: null, card: null,
content: replacedContent, content: replacedContent,
emojis: data.emojis.map((emoji) => new Emoji(emoji).toApi()), emojis: data.emojis.map((emoji) => new Emoji(emoji).toApi()),

View file

@ -16,6 +16,7 @@ import {
userRelations, userRelations,
} from "../functions/user.ts"; } from "../functions/user.ts";
import { Account } from "../schemas/account.ts"; import { Account } from "../schemas/account.ts";
import { Status } from "../schemas/status.ts";
import { BaseInterface } from "./base.ts"; import { BaseInterface } from "./base.ts";
export type NotificationType = InferSelectModel<typeof Notifications> & { export type NotificationType = InferSelectModel<typeof Notifications> & {
@ -31,7 +32,7 @@ export class Notification extends BaseInterface<
account: Account.nullable(), account: Account.nullable(),
created_at: z.string(), created_at: z.string(),
id: z.string().uuid(), id: z.string().uuid(),
status: z.lazy(() => Note.schema).optional(), status: Status.optional(),
// TODO: Add reactions // TODO: Add reactions
type: z.enum([ type: z.enum([
"mention", "mention",

View file

@ -3,6 +3,7 @@ import { getBestContentType, urlToContentFormat } from "@/content_types";
import { randomString } from "@/math"; import { randomString } from "@/math";
import { proxyUrl } from "@/response"; import { proxyUrl } from "@/response";
import { sentry } from "@/sentry"; import { sentry } from "@/sentry";
import type { z } from "@hono/zod-openapi";
import { getLogger } from "@logtape/logtape"; import { getLogger } from "@logtape/logtape";
import type { Mention as ApiMention } from "@versia/client/types"; import type { Mention as ApiMention } from "@versia/client/types";
import { import {
@ -45,7 +46,6 @@ import {
sql, sql,
} from "drizzle-orm"; } from "drizzle-orm";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import type { z } from "zod";
import { findManyUsers } from "~/classes/functions/user"; import { findManyUsers } from "~/classes/functions/user";
import { searchManager } from "~/classes/search/search-manager"; import { searchManager } from "~/classes/search/search-manager";
import { type Config, config } from "~/packages/config-manager"; import { type Config, config } from "~/packages/config-manager";

View file

@ -75,6 +75,7 @@ export const Source = z
follow_requests_count: z follow_requests_count: z
.number() .number()
.int() .int()
.nonnegative()
.optional() .optional()
.openapi({ .openapi({
description: "The number of pending follow requests.", description: "The number of pending follow requests.",
@ -352,8 +353,8 @@ export const Account = z.object({
.nullable(), .nullable(),
statuses_count: z statuses_count: z
.number() .number()
.min(0)
.int() .int()
.nonnegative()
.openapi({ .openapi({
description: "How many statuses are attached to this account.", description: "How many statuses are attached to this account.",
example: 42, example: 42,
@ -363,8 +364,8 @@ export const Account = z.object({
}), }),
followers_count: z followers_count: z
.number() .number()
.min(0)
.int() .int()
.nonnegative()
.openapi({ .openapi({
description: "The reported followers of this profile.", description: "The reported followers of this profile.",
example: 6, example: 6,
@ -374,8 +375,8 @@ export const Account = z.object({
}), }),
following_count: z following_count: z
.number() .number()
.min(0)
.int() .int()
.nonnegative()
.openapi({ .openapi({
description: "The reported follows of this profile.", description: "The reported follows of this profile.",
example: 23, example: 23,

View file

@ -0,0 +1,71 @@
import { z } from "@hono/zod-openapi";
export const Application = z.object({
name: z.string().openapi({
description: "The name of your application.",
example: "Test Application",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Application/#name",
},
}),
website: z
.string()
.nullable()
.openapi({
description: "The website associated with your application.",
example: "https://app.example",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Application/#website",
},
}),
scopes: z.array(z.string()).openapi({
description:
"The scopes for your application. This is the registered scopes string split on whitespace.",
example: ["read", "write", "push"],
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Application/#scopes",
},
}),
redirect_uris: z
.array(
z.string().url().openapi({
description: "URL or 'urn:ietf:wg:oauth:2.0:oob'",
}),
)
.openapi({
description:
"The registered redirection URI(s) for your application.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Application/#redirect_uris",
},
}),
redirect_uri: z.string().openapi({
description:
"The registered redirection URI(s) for your application. May contain \\n characters when multiple redirect URIs are registered.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Application/#redirect_uri",
},
}),
});
export const CredentialApplication = Application.extend({
client_id: z.string().openapi({
description: "Client ID key, to be used for obtaining OAuth tokens",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_id",
},
}),
client_secret: z.string().openapi({
description: "Client secret key, to be used for obtaining OAuth tokens",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_secret",
},
}),
client_secret_expires_at: z.string().openapi({
description:
"When the client secret key will expire at, presently this always returns 0 indicating that OAuth Clients do not expire",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_secret_expires_at",
},
}),
});

151
classes/schemas/card.ts Normal file
View file

@ -0,0 +1,151 @@
import { z } from "@hono/zod-openapi";
import { Account } from "./account.ts";
export const PreviewCardAuthor = z.object({
name: z.string().openapi({
description: "The original resource authors name.",
example: "The Doubleclicks",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#name",
},
}),
url: z
.string()
.url()
.openapi({
description: "A link to the author of the original resource.",
example: "https://www.youtube.com/user/thedoubleclicks",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#url",
},
}),
account: Account.nullable().openapi({
description: "The fediverse account of the author.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#account",
},
}),
});
export const PreviewCard = z.object({
url: z
.string()
.url()
.openapi({
description: "Location of linked resource.",
example: "https://www.youtube.com/watch?v=OMv_EPMED8Y",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#url",
},
}),
title: z
.string()
.min(1)
.openapi({
description: "Title of linked resource.",
example: "♪ Brand New Friend (Christmas Song!)",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#title",
},
}),
description: z.string().openapi({
description: "Description of preview.",
example: "",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#description",
},
}),
type: z.enum(["link", "photo", "video"]).openapi({
description: "The type of the preview card.",
example: "video",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#type",
},
}),
authors: z.array(PreviewCardAuthor).openapi({
description:
"Fediverse account of the authors of the original resource.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#authors",
},
}),
provider_name: z.string().openapi({
description: "The provider of the original resource.",
example: "YouTube",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#provider_name",
},
}),
provider_url: z
.string()
.url()
.openapi({
description: "A link to the provider of the original resource.",
example: "https://www.youtube.com/",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#provider_url",
},
}),
html: z.string().openapi({
description: "HTML to be used for generating the preview card.",
example:
'<iframe width="480" height="270" src="https://www.youtube.com/embed/OMv_EPMED8Y?feature=oembed" frameborder="0" allowfullscreen=""></iframe>',
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#html",
},
}),
width: z
.number()
.int()
.openapi({
description: "Width of preview, in pixels.",
example: 480,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#width",
},
}),
height: z
.number()
.int()
.openapi({
description: "Height of preview, in pixels.",
example: 270,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#height",
},
}),
image: z
.string()
.url()
.nullable()
.openapi({
description: "Preview thumbnail.",
example:
"https://cdn.versia.social/preview_cards/images/014/179/145/original/9cf4b7cf5567b569.jpeg",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#image",
},
}),
embed_url: z
.string()
.url()
.openapi({
description: "Used for photo embeds, instead of custom html.",
example:
"https://live.staticflickr.com/65535/49088768431_6a4322b3bb_b.jpg",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#embed_url",
},
}),
blurhash: z
.string()
.nullable()
.openapi({
description:
"A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.",
example: "UvK0HNkV,:s9xBR%njog0fo2W=WBS5ozofV@",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#blurhash",
},
}),
});

View file

@ -0,0 +1,3 @@
import { z } from "@hono/zod-openapi";
export const Id = z.string().uuid();

129
classes/schemas/filters.ts Normal file
View file

@ -0,0 +1,129 @@
import { z } from "@hono/zod-openapi";
import { Id } from "./common.ts";
export const FilterStatus = z.object({
id: Id.openapi({
description: "The ID of the FilterStatus in the database.",
example: "3b19ed7c-0c4b-45e1-8c75-e21dfc8e86c3",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterStatus/#id",
},
}),
status_id: Id.openapi({
description: "The ID of the Status that will be filtered.",
example: "4f941ac8-295c-4c2d-9300-82c162ac8028",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterStatus/#status_id",
},
}),
});
export const FilterKeyword = z.object({
id: Id.openapi({
description: "The ID of the FilterKeyword in the database.",
example: "ca921e60-5b96-4686-90f3-d7cc420d7391",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterKeyword/#id",
},
}),
keyword: z.string().openapi({
description: "The phrase to be matched against.",
example: "badword",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterKeyword/#keyword",
},
}),
whole_word: z.boolean().openapi({
description:
"Should the filter consider word boundaries? See implementation guidelines for filters.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterKeyword/#whole_word",
},
}),
});
export const Filter = z.object({
id: Id.openapi({
description: "The ID of the Filter in the database.",
example: "6b8fa22f-b128-43c2-9a1f-3c0499ef3a51",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#id",
},
}),
title: z.string().openapi({
description: "A title given by the user to name the filter.",
example: "Test filter",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#title",
},
}),
context: z
.array(z.enum(["home", "notifications", "public", "thread", "account"]))
.openapi({
description: "The contexts in which the filter should be applied.",
example: ["home"],
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#context",
},
}),
expires_at: z
.string()
.nullable()
.openapi({
description: "When the filter should no longer be applied.",
example: "2026-09-20T17:27:39.296Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#expires_at",
},
}),
filter_action: z.enum(["warn", "hide"]).openapi({
description:
"The action to be taken when a status matches this filter.",
example: "warn",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#filter_action",
},
}),
keywords: z.array(FilterKeyword).openapi({
description: "The keywords grouped under this filter.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#keywords",
},
}),
statuses: z.array(FilterStatus).openapi({
description: "The statuses grouped under this filter.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#statuses",
},
}),
});
export const FilterResult = z.object({
filter: Filter.openapi({
description: "The filter that was matched.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterResult/#filter",
},
}),
keyword_matches: z
.array(z.string())
.nullable()
.openapi({
description: "The keyword within the filter that was matched.",
example: ["badword"],
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterResult/#keyword_matches",
},
}),
status_matches: z
.array(Id)
.nullable()
.openapi({
description: "The status ID within the filter that was matched.",
example: ["3819515a-5ceb-4078-8524-c939e38dcf8f"],
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterResult/#status_matches",
},
}),
});

490
classes/schemas/status.ts Normal file
View file

@ -0,0 +1,490 @@
import { z } from "@hono/zod-openapi";
import type { Status as ApiNote } from "@versia/client/types";
import { Emoji, Media } from "@versia/kit/db";
import ISO6391 from "iso-639-1";
import { zBoolean } from "~/packages/config-manager/config.type.ts";
import { Account } from "./account.ts";
import { PreviewCard } from "./card.ts";
import { Id } from "./common.ts";
import { FilterResult } from "./filters.ts";
export const Mention = z
.object({
id: Account.shape.id.openapi({
description: "The account ID of the mentioned user.",
example: "b9dcb548-bd4d-42af-8b48-3693e6d298e6",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention-id",
},
}),
username: Account.shape.username.openapi({
description: "The username of the mentioned user.",
example: "lexi",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention-username",
},
}),
url: Account.shape.url.openapi({
description: "The location of the mentioned users profile.",
example: "https://beta.versia.social/@lexi",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention-url",
},
}),
acct: Account.shape.acct.openapi({
description:
"The webfinger acct: URI of the mentioned user. Equivalent to username for local users, or username@domain for remote users.",
example: "lexi@beta.versia.social",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention-acct",
},
}),
})
.openapi({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention",
},
});
export const Tag = z
.object({
name: z
.string()
.min(1)
.max(128)
.openapi({
description: "The value of the hashtag after the # sign.",
example: "versia",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Tag-name",
},
}),
url: z
.string()
.url()
.openapi({
description: "A link to the hashtag on the instance.",
example: "https://beta.versia.social/tags/versia",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Tag-url",
},
}),
})
.openapi({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Tag",
},
});
export const PollOption = z
.object({
title: z.string().openapi({
description: "The text value of the poll option.",
example: "yes",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#Option-title",
},
}),
votes_count: z
.number()
.int()
.nonnegative()
.nullable()
.openapi({
description:
"The total number of received votes for this option.",
example: 6,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#Option-votes_count",
},
}),
})
.openapi({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#Option",
},
});
export const Poll = z.object({
id: Id.openapi({
description: "ID of the poll in the database.",
example: "d87d230f-e401-4282-80c7-2044ab989662",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#id",
},
}),
expires_at: z
.string()
.datetime()
.nullable()
.openapi({
description: "When the poll ends.",
example: "2025-01-07T14:11:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#expires_at",
},
}),
expired: zBoolean.openapi({
description: "Is the poll currently expired?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#expired",
},
}),
multiple: zBoolean.openapi({
description: "Does the poll allow multiple-choice answers?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#multiple",
},
}),
votes_count: z
.number()
.int()
.nonnegative()
.openapi({
description: "How many votes have been received.",
example: 6,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#votes_count",
},
}),
voters_count: z
.number()
.int()
.nonnegative()
.nullable()
.openapi({
description:
"How many unique accounts have voted on a multiple-choice poll.",
example: 3,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#voters_count",
},
}),
options: z.array(PollOption).openapi({
description: "Possible answers for the poll.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#options",
},
}),
emojis: z.array(Emoji.schema).openapi({
description: "Custom emoji to be used for rendering poll options.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#emojis",
},
}),
voted: zBoolean.optional().openapi({
description:
"When called with a user token, has the authorized user voted?",
example: true,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#voted",
},
}),
own_votes: z
.array(z.number().int())
.optional()
.openapi({
description:
"When called with a user token, which options has the authorized user chosen? Contains an array of index values for options.",
example: [0],
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#own_votes",
},
}),
});
export const Status = z.object({
id: Id.openapi({
description: "ID of the status in the database.",
example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#id",
},
}),
uri: z
.string()
.url()
.openapi({
description: "URI of the status used for federation.",
example:
"https://beta.versia.social/@lexi/2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#uri",
},
}),
url: z
.string()
.url()
.nullable()
.openapi({
description: "A link to the statuss HTML representation.",
example:
"https://beta.versia.social/@lexi/2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#url",
},
}),
account: Account.openapi({
description: "The account that authored this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#account",
},
}),
in_reply_to_id: Id.nullable().openapi({
description: "ID of the status being replied to.",
example: "c41c9fe9-919a-4d35-a921-d3e79a5c95f8",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#in_reply_to_id",
},
}),
in_reply_to_account_id: Account.shape.id.nullable().openapi({
description:
"ID of the account that authored the status being replied to.",
example: "7b9b3ec6-1013-4cc6-8902-94ad00cf2ccc",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#in_reply_to_account_id",
},
}),
reblog: z
.lazy((): z.ZodType<ApiNote> => Status as z.ZodType<ApiNote>)
.nullable()
.openapi({
description: "The status being reblogged.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#reblog",
},
}),
content: z.string().openapi({
description: "HTML-encoded status content.",
example: "<p>hello world</p>",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#content",
},
}),
created_at: z
.string()
.datetime()
.openapi({
description: "The date when this status was created.",
example: "2025-01-07T14:11:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#created_at",
},
}),
edited_at: z
.string()
.datetime()
.nullable()
.openapi({
description: "Timestamp of when the status was last edited.",
example: "2025-01-07T14:11:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#edited_at",
},
}),
emojis: z.array(Emoji.schema).openapi({
description: "Custom emoji to be used when rendering status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#emojis",
},
}),
replies_count: z
.number()
.int()
.nonnegative()
.openapi({
description: "How many replies this status has received.",
example: 1,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#replies_count",
},
}),
reblogs_count: z
.number()
.int()
.nonnegative()
.openapi({
description: "How many boosts this status has received.",
example: 6,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#reblogs_count",
},
}),
favourites_count: z
.number()
.int()
.nonnegative()
.openapi({
description: "How many favourites this status has received.",
example: 11,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#favourites_count",
},
}),
reblogged: zBoolean.optional().openapi({
description:
"If the current token has an authorized user: Have you boosted this status?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#reblogged",
},
}),
favourited: zBoolean.optional().openapi({
description:
"If the current token has an authorized user: Have you favourited this status?",
example: true,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#favourited",
},
}),
muted: zBoolean.optional().openapi({
description:
"If the current token has an authorized user: Have you muted notifications for this statuss conversation?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#muted",
},
}),
sensitive: zBoolean.openapi({
description: "Is this status marked as sensitive content?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#sensitive",
},
}),
spoiler_text: z.string().openapi({
description:
"Subject or summary line, below which status content is collapsed until expanded.",
example: "lewd text",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#spoiler_text",
},
}),
visibility: z.enum(["public", "unlisted", "private", "direct"]).openapi({
description: "Visibility of this status.",
example: "public",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#visibility",
},
}),
media_attachments: z.array(Media.schema).openapi({
description: "Media that is attached to this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#media_attachments",
},
}),
mentions: z.array(Mention).openapi({
description: "Mentions of users within the status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#mentions",
},
}),
tags: z.array(Tag).openapi({
description: "Hashtags used within the status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#tags",
},
}),
card: PreviewCard.nullable().openapi({
description: "Preview card for links included within status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#card",
},
}),
poll: Poll.nullable().openapi({
description: "The poll attached to the status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#poll",
},
}),
application: z
.object({
name: z.string().openapi({
description:
"The name of the application that posted this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#application-name",
},
}),
website: z
.string()
.url()
.nullable()
.openapi({
description:
"The website associated with the application that posted this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#application-website",
},
}),
})
.optional()
.openapi({
description: "The application used to post this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#application",
},
}),
language: z
.enum(ISO6391.getAllCodes() as [string, ...string[]])
.nullable()
.openapi({
description: "Primary language of this status.",
example: "en",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#language",
},
}),
text: z
.string()
.nullable()
.openapi({
description:
"Plain-text source of a status. Returned instead of content when status is deleted, so the user may redraft from the source text without the client having to reverse-engineer the original text from the HTML content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#text",
},
}),
pinned: zBoolean.optional().openapi({
description:
"If the current token has an authorized user: Have you pinned this status? Only appears if the status is pinnable.",
example: true,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#pinned",
},
}),
emoji_reactions: z.array(
z.object({
count: z.number().int().nonnegative(),
me: zBoolean,
name: z.string(),
url: z.string().url().optional(),
static_url: z.string().url().optional(),
accounts: z.array(Account).optional(),
account_ids: z.array(z.string().uuid()).optional(),
}),
),
quote: z
.lazy((): z.ZodType<ApiNote> => Status as z.ZodType<ApiNote>)
.nullable(),
bookmarked: zBoolean.optional().openapi({
description:
"If the current token has an authorized user: Have you bookmarked this status?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#bookmarked",
},
}),
filtered: z
.array(FilterResult)
.optional()
.openapi({
description:
"If the current token has an authorized user: The filter and keywords that matched this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#filtered",
},
}),
});

View file

@ -156,7 +156,7 @@ describe("API Tests", () => {
expect(statusJson.created_at).toBeDefined(); expect(statusJson.created_at).toBeDefined();
expect(statusJson.account).toBeDefined(); expect(statusJson.account).toBeDefined();
expect(statusJson.reblog).toBeDefined(); expect(statusJson.reblog).toBeDefined();
expect(statusJson.application).toBeDefined(); expect(statusJson.application).toBeUndefined();
expect(statusJson.emojis).toBeDefined(); expect(statusJson.emojis).toBeDefined();
expect(statusJson.media_attachments).toBeDefined(); expect(statusJson.media_attachments).toBeDefined();
expect(statusJson.poll).toBeDefined(); expect(statusJson.poll).toBeDefined();