api: define the rest #33
232
api/main.tsp
232
api/main.tsp
@ -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;
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
reason: string;
|
||||||
|
|
||||||
|
/** Time the ban expires and the user regains full access to Ligmotes. */
|
||||||
until: utcDateTime;
|
until: utcDateTime;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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
@ -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]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user