api: define the rest #33

Merged
zephyr merged 3 commits from api-finish into main 2025-05-28 18:32:19 -04:00
6 changed files with 2849 additions and 183 deletions

View File

@ -73,7 +73,7 @@ namespace Read {
@get @get
@operationId("GetSet") @operationId("GetSet")
@route("/sets/{setID}") @route("/sets/{setID}")
op set(@path setID: id, @query mediaType?: mediaType = mediaType.avif): SetResp | SetNotFound; op set(@path setID: id, @query mediaType?: mediaType = mediaType.webp): SetResp | SetNotFound;
/** /**
* Operations for connected platforms. * Operations for connected platforms.
@ -100,7 +100,7 @@ namespace Read {
@get @get
@operationId("GetPlatformSet") @operationId("GetPlatformSet")
@route("/set") @route("/set")
op set(...platformParams, @query mediaType?: mediaType = mediaType.avif): SetResp | SetNotFound; op set(...platformParams, @query mediaType?: mediaType = mediaType.webp): SetResp | SetNotFound;
/** /**
* Report used emotes. * Report used emotes.
@ -132,12 +132,15 @@ namespace Read {
@useAuth(BearerAuth) @useAuth(BearerAuth)
namespace EmoteManagement { namespace EmoteManagement {
/** /**
* Image types accepted in uploads. * Image content for an emote upload.
*/ */
model Image is File<"image/png" | "image/jpeg" | "image/gif" | "image/webp" | "image/avif">; model Image {
format: mediaType;
data: bytes;
}
/** Created emote. */ /** Created emote. */
model CreatedEmote is Success<Emote, 202>; model CreatedEmote is Success<Emote>;
/** Updated emote. */ /** Updated emote. */
model UpdatedEmote is Success<Emote>; model UpdatedEmote is Success<Emote>;
@ -147,20 +150,12 @@ namespace EmoteManagement {
model ErrExcessiveUpload is Error<413>; model ErrExcessiveUpload is Error<413>;
/** /**
* Upload an emote. * Create a new emote.
*/ */
@post @post
@operationId("CreateEmote") @operationId("CreateEmote")
@route("/emotes") @route("/emotes")
op new( op new(emote: Emote): CreatedEmote | ErrBadParameters | ErrAuth | ErrExcessiveUpload;
@header contentType: "multipart/form-data",
@multipartBody fields: {
meta: HttpPart<Create<Emote> & {
@header contentType: "application/json";
}>;
image: HttpPart<Image>;
},
): CreatedEmote | ErrBadParameters | ErrAuth | ErrExcessiveUpload;
/** /**
* Modify an existing emote. * Modify an existing emote.
@ -178,3 +173,210 @@ namespace EmoteManagement {
@route("/emotes/{emoteID}") @route("/emotes/{emoteID}")
op delete(@path emoteID: id): NoContentResponse | ErrAuth; op delete(@path emoteID: id): NoContentResponse | ErrAuth;
} }
/**
* The Ligmotes User Management API.
*/
@useAuth(BearerAuth)
namespace UserManagement {
// NOTE(zephyr): creating users goes through the auth api, not this
/**
* Update a user's name or picture.
* Omitting the picture field in the request body causes the user's picture
* to be removed.
*/
@patch(#{ implicitOptionality: false })
@operationId("UpdateUser")
@route("/users/{userID}")
op update(@path userID: id, @body user: User): Success<User> | ErrAuth | ErrBadParameters | ErrNotFound;
/**
* Soft-delete a user.
*
* Typically called by a user for themselves; moderators would use the
* /moderate/user API to ban them instead.
* This preserves the user's uploaded emotes as well as any moderation
* reviews they may have performed.
*/
@delete
@operationId("DeleteUser")
@route("/users/{userID}")
op delete(@path userID: id): Success<User> | ErrAuth | ErrNotFound;
}
/**
* The Ligmotes Chat Management API.
* Relates to managing emote sets and connections.
*/
@useAuth(BearerAuth)
namespace ChatManagement {
/**
* ID and name of an emote within a set.
* The same ID can appear multiple times in one set, but names are unique.
*/
model SetEmote {
id: id;
name: string;
/** RenameEmote may specify a new name for an existing emote in the set. */
rename?: string;
}
/**
* Create a new emote set.
* Note that creating a user should automatically create their first emote
* set as well, without needing a call to this operation.
*/
@post
@operationId("CreateEmoteSet")
@route("/sets")
op create(@body emoteSet: EmoteSet): Success<EmoteSet> | ErrAuth | ErrBadParameters;
/** Update the name of an existing emote set. */
@patch(#{ implicitOptionality: false })
@operationId("UpdateEmoteSet")
@route("/sets/{setID}")
op update(@path setID: id, @body emoteSet: EmoteSet): Success<EmoteSet> | ErrAuth | ErrBadParameters | ErrNotFound;
// NOTE(zephyr): We do not allow deleting emote sets.
// That would enable many techniques to circumvent limits.
/** Add an emote to an emote set. */
// TODO(zephyr): is returning the entire updated emote set too expensive?
@post
@operationId("AddEmote")
@route("/sets/{setID}/emotes")
op addEmote(@path setID: id, @body emote: SetEmote):
| Success<EmoteSet>
| ErrAuth
| ErrBadParameters
| ErrNotFound
| ErrDuplicate;
/** Rename an emote that is already in an emote set. */
@patch(#{ implicitOptionality: false })
@operationId("RenameEmote")
@route("/sets/{setID}/emotes")
op renameEmote(@path setID: id, @body emote: SetEmote):
| Success<EmoteSet>
| ErrAuth
| ErrBadParameters
| ErrNotFound
| ErrDuplicate;
/**
* Remove an emote from an emote set.
* This performs a soft-delete of the association between the two.
*/
@delete
@operationId("RemoveEmote")
@route("/sets/{setID}/emotes")
op removeEmote(@path setID: id, @body emote: SetEmote): Success<EmoteSet> | ErrAuth | ErrNotFound;
/** Give a user editor permission for an emote set. */
@post
@operationId("AddEditor")
@route("/sets/{setID}/editors")
op addEditor(
@path setID: id,
@body editor: {
userID: id;
},
): Success<EmoteSet> | ErrAuth | ErrNotFound | ErrDuplicate;
/** Remove a user's permission to edit an emote set. */
@delete
@operationId("RemoveEditor")
@route("/sets/{setID}/editors")
op removeEditor(
@path setID: id,
@body editor: {
userID: id;
},
): Success<EmoteSet> | ErrAuth | ErrNotFound;
/** Get emote sets that the user is allowed to edit. */
// TODO(zephyr): we should report this as a field on emote set objects as well
@get
@operationId("ListEditableSets")
@route("/users/{userID}/sets")
op listEditableSets(@path userID: id): Success<EmoteSet[]> | ErrAuth | ErrBadParameters;
/** Set an emote set as active for a connection. */
@patch(#{ implicitOptionality: false })
@operationId("UpdateActiveSet")
@route("/platforms/{platform}/connections/{connectedID}/set")
op updateActiveSet(
@path platform: platform,
@path connectedID: string,
@body update: {
setID: id;
},
): Success<EmoteSet> | ErrAuth | ErrBadParameters;
}
/**
* Moderation API.
*/
@useAuth(BearerAuth)
@route("/moderate")
namespace Moderation {
/** Request body for updating emote approval status. */
model EmoteStatus {
approved: boolean;
}
/** Options for deleting an emote. */
model EmoteDeleteOptions {
deleteMedia?: boolean;
}
/**
* List a selection of emotes awaiting moderator review.
* NOTE(zephyr): Unlike other APIs, this returns the emotes directly.
* We want all the information for reviewing, and a bit extra latency for
* the join is less of an issue than it is in chat.
*/
@get
@operationId("ListUnreviewedEmotes")
@route("/emotes")
op listUnreviewed(@query limit?: integer = 100): Success<Emote[]> | ErrAuth;
/**
* Update an emote's approval status.
*/
@post
@operationId("UpdateEmoteStatus")
@route("/emotes/{emoteID}")
op updateEmoteStatus(@path emoteID: id, @body status: EmoteStatus):
| Success<Emote>
| ErrAuth
| ErrBadParameters
| ErrNotFound;
/**
* Fully remove an emote, e.g. for abusive or illegal content.
*
* This is a hard delete.
* Typically, a call to it should have an accompanying call to ban the user.
*/
@delete
@operationId("HardDeleteEmote")
@route("/emotes/{emoteID}")
op hardDeleteEmote(@path emoteID: id, @body options?: EmoteDeleteOptions):
| NoContentResponse
| ErrAuth
| ErrBadParameters
| ErrNotFound;
/**
* Ban a user for a given reason.
* This revokes all the user's current sessions, removes their active emote
* sets, and enforces the restrictions described on UserBan.
*/
@post
@operationId("Ban")
@route("/users/{userID}")
op ban(@path userID: id, @body details: UserBan): Success<User> | ErrAuth | ErrBadParameters | ErrNotFound;
}

View File

@ -23,7 +23,7 @@ model Success<T, Code extends 200 | 202 = 200> {
* Error response. * Error response.
*/ */
@error @error
model Error<Code extends 400 | 401 | 403 | 404 | 405 | 413 | 500> { model Error<Code extends 400 | 401 | 403 | 404 | 405 | 409 | 413 | 429 | 500> {
@statusCode @statusCode
code: Code; code: Code;
@ -51,6 +51,14 @@ model ErrBadParameters is Error<400>;
@error @error
model ErrAuth is Error<401 | 403>; model ErrAuth is Error<401 | 403>;
/** Request specified an ID for a resource which does not exist. */
@error
model ErrNotFound is Error<404>;
/** Request tried to add an association which already exists. */
@error
model ErrDuplicate is Error<409>;
/** /**
* ID of a user, emote, emote list, &c. * ID of a user, emote, emote list, &c.
* UUIDv7. * UUIDv7.
@ -76,7 +84,7 @@ model Emote {
name: string; name: string;
/** Links to and metadata about the available emote images. */ /** Links to and metadata about the available emote images. */
@visibility(Lifecycle.Read) @visibility(Lifecycle.Read, Lifecycle.Create)
media: Media[]; media: Media[];
/** Emote color for sorting. */ /** Emote color for sorting. */
@ -99,7 +107,7 @@ model Emote {
@visibility(Lifecycle.Read, Lifecycle.Create, Lifecycle.Update) @visibility(Lifecycle.Read, Lifecycle.Create, Lifecycle.Update)
attributions: Attribution[]; attributions: Attribution[];
/** List of channels allowed to use the emote, if so restricted. */ /** List of users allowed to use the emote, if so restricted. */
@visibility(Lifecycle.Read, Lifecycle.Create, Lifecycle.Update) @visibility(Lifecycle.Read, Lifecycle.Create, Lifecycle.Update)
restrictions?: id[]; restrictions?: id[];
@ -145,12 +153,28 @@ model MinEmote {
* Link and metadata for an emote image. * Link and metadata for an emote image.
*/ */
model Media { model Media {
@visibility(Lifecycle.Read, Lifecycle.Create)
format: mediaType; format: mediaType;
@visibility(Lifecycle.Read, Lifecycle.Create)
scale: 1 | 2 | 4; scale: 1 | 2 | 4;
@visibility(Lifecycle.Create)
data: bytes;
@visibility(Lifecycle.Read)
hash: string; hash: string;
@visibility(Lifecycle.Read)
url: url; url: url;
@visibility(Lifecycle.Read)
height: int16; height: int16;
@visibility(Lifecycle.Read)
width: int16; width: int16;
@visibility(Lifecycle.Read)
filesize: int32; filesize: int32;
} }
@ -158,7 +182,7 @@ model Media {
* Media formats that the CDN delivers. * Media formats that the CDN delivers.
*/ */
enum mediaType { enum mediaType {
avif: "AVIF", webp: "WebP",
} }
/** /**
@ -198,10 +222,22 @@ model User {
/** User ban information. Only shown to the user themselves. */ /** User ban information. Only shown to the user themselves. */
@visibility(Lifecycle.Read) @visibility(Lifecycle.Read)
ban?: { ban?: UserBan;
reason: string; }
until: utcDateTime;
}; /**
* User ban details.
* A banned user cannot use any authenticated APIs, meaning they cannot upload
* or edit emotes, manage emote sets, or moderate.
* They can still use the Read APIs and can still be issued sessions, allowing
* them to sign in and see their ban reason.
*/
model UserBan {
/** Reason for the ban. This is shown to the banned user. */
reason: string;
/** Time the ban expires and the user regains full access to Ligmotes. */
until: utcDateTime;
} }
/** /**

File diff suppressed because it is too large Load Diff

View File

@ -5,4 +5,4 @@ options:
emitter-output-dir: "{output-dir}/schema" emitter-output-dir: "{output-dir}/schema"
openapi-versions: openapi-versions:
- 3.1.0 - 3.1.0
output-dir: "{cwd}" output-dir: "{project-root}"

File diff suppressed because it is too large Load Diff

View File

@ -80,7 +80,8 @@ run = [
[tasks."gen:api"] [tasks."gen:api"]
description = "Generate API scaffold" description = "Generate API scaffold"
run = [ run = [
'go tool oapi-codegen -config ./backend/rest/config.yaml ./api/schema/openapi.yaml', 'tsp compile ./api',
'go tool oapi-codegen -config ./backend/rest/config.yaml ./api/schema/openapi.v1.yaml',
] ]
[env] [env]