v3: shared/ is documented like api/ — deno doc --lint over api/ and shared/ goes from 385 errors to none — every module of shared/ opens with a /** … @module */ block; every export and every field of an exported interface has JSDoc, each request body naming the endpoint that takes it and each response its endpoint; the // comments that described declarations (the mention pattern, the permission union and table, four socket messages, the error message's optimistic-action context) are /** */ now, and the socket protocol's heading comment, which sat on no declaration, is its module doc. The lint wanted explicit types on MENTION_RE and DUMP_MAX_FILE_SIZE_BYTES. Along the way: the chat edit is allowed to moderators too, which the new ChatEditMessage doc says; UpdateUserRequest says that PATCH /api/users/me takes only its description and email; playlist-service's reorderPlaylist @throws is rewrapped. mod.ts and the README no longer call shared/ undocumented. No behaviour changes; 205 backend tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
29
README.md
29
README.md
@@ -158,23 +158,22 @@ place that fetches from other sites. Tests mirror the source one file per
|
||||
module, so the tests of `services/chat-service.ts` are in
|
||||
`test/services/chat-service.test.ts`.
|
||||
|
||||
The backend is documented for `deno doc`, the same way in every file. Each
|
||||
module opens with a `/** … @module */` block saying what it is for. Every
|
||||
export has a JSDoc comment, and so does every field of an exported interface; a
|
||||
comment that describes a declaration is always `/** */`, and `//` is only for
|
||||
notes on the code inside. Functions open with a verb ("Returns…", "Checks…"),
|
||||
everything else with a noun phrase. Parameters are named in the prose, in
|
||||
backticks. `{@link}` points at symbols the file already has in scope. A
|
||||
function that can throw to its caller says so with `@throws`: for an
|
||||
The backend and `shared/` are documented for `deno doc`, the same way in every
|
||||
file. Each module opens with a `/** … @module */` block saying what it is for.
|
||||
Every export has a JSDoc comment, and so does every field of an exported
|
||||
interface; a comment that describes a declaration is always `/** */`, and `//`
|
||||
is only for notes on the code inside. Functions open with a verb ("Returns…",
|
||||
"Checks…"), everything else with a noun phrase. Parameters are named in the
|
||||
prose, in backticks. `{@link}` points at symbols the file already has in scope.
|
||||
A function that can throw to its caller says so with `@throws`: for an
|
||||
`APIException`, the statuses and when. A router's JSDoc lists its endpoints, one
|
||||
line each: method and path, who may call it, what it does. `deno task doc`
|
||||
builds the HTML reference into `docs/` from every module of `api/` and
|
||||
`shared/` but the tests. Its index page is the module doc of `mod.ts` at the
|
||||
root, which introduces the repository's three parts and links every documented
|
||||
module (`mod.test.ts` checks it), and the `docs` workflow publishes it on every
|
||||
push to `main` that touches them. `deno task doc:lint` checks
|
||||
the rules the linter can see; `shared/` is not documented yet, so it still
|
||||
fails there.
|
||||
builds the HTML reference into `docs/` from every module of `api/` and `shared/`
|
||||
but the tests. Its index page is the module doc of `mod.ts` at the root, which
|
||||
introduces the repository's three parts and links every documented module
|
||||
(`mod.test.ts` checks it), and the `docs` workflow publishes it on every push to
|
||||
`main` that touches them. `deno task doc:lint` checks the rules the linter can
|
||||
see.
|
||||
|
||||
Each of the frontend's `components/`, `contexts/`, `hooks/` and `utils/` is
|
||||
split into the same domain folders — `auth`, `category`, `chat`, `comment`,
|
||||
|
||||
@@ -406,7 +406,8 @@ export function removeDumpFromPlaylist(
|
||||
* Puts the playlist's members the requester can see in the order of
|
||||
* `dumpIds`, and announces the change.
|
||||
*
|
||||
* @throws {APIException} 404 when there is no such playlist, 403 when the user is neither its owner nor a moderator; 400 when `dumpIds` isn't exactly the
|
||||
* @throws {APIException} 404 when there is no such playlist, 403 when the user
|
||||
* is neither its owner nor a moderator, 400 when `dumpIds` isn't exactly the
|
||||
* members they can see.
|
||||
*/
|
||||
export function reorderPlaylist(
|
||||
|
||||
3
mod.ts
3
mod.ts
@@ -137,8 +137,7 @@
|
||||
*
|
||||
* The types and constants both halves use: the API envelope, each domain's
|
||||
* types and request bodies, the socket protocol, the validation limits and the
|
||||
* role permissions. It imports nothing from either half. Its modules aren't
|
||||
* documented yet.
|
||||
* role permissions. It imports nothing from either half.
|
||||
*
|
||||
* Modules: [api](./shared/api.ts/index.html),
|
||||
* [category](./shared/category.ts/index.html),
|
||||
|
||||
@@ -1,39 +1,66 @@
|
||||
/**
|
||||
* The envelope every endpoint answers with, and the error codes it carries.
|
||||
* The envelope every endpoint answers with, the error codes it carries, the
|
||||
* shape of a paginated list, and {@link Wire}, which maps a type to what JSON
|
||||
* makes of it.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** The error codes an {@link APIError} carries, by name. */
|
||||
export const APIErrorCode = {
|
||||
/** The request is malformed or not allowed in the current state. */
|
||||
BAD_REQUEST: "BAD_REQUEST",
|
||||
/** What the request names doesn't exist, or the caller may not see it. */
|
||||
NOT_FOUND: "NOT_FOUND",
|
||||
/** The server failed. */
|
||||
SERVER_ERROR: "SERVER_ERROR",
|
||||
/** A remote site took too long to answer. */
|
||||
TIMEOUT: "TIMEOUT",
|
||||
/** The caller isn't signed in, or may not do this. */
|
||||
UNAUTHORIZED: "UNAUTHORIZED",
|
||||
/** The request body failed validation. */
|
||||
VALIDATION_ERROR: "VALIDATION_ERROR",
|
||||
} as const;
|
||||
|
||||
/** One of the {@link APIErrorCode} values. */
|
||||
export type APIErrorCode = typeof APIErrorCode[keyof typeof APIErrorCode];
|
||||
|
||||
/** What went wrong, in a failed response. */
|
||||
export interface APIError {
|
||||
/** The kind of failure. */
|
||||
code: APIErrorCode;
|
||||
/** A human-readable explanation. */
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** A successful response, carrying its payload. */
|
||||
export interface APISuccess<T> {
|
||||
/** Always true. */
|
||||
success: true;
|
||||
/** The payload. */
|
||||
data: T;
|
||||
/** Never set on success. */
|
||||
error?: never;
|
||||
}
|
||||
|
||||
/** A failed response, carrying its error. */
|
||||
export interface APIFailure {
|
||||
/** Always false. */
|
||||
success: false;
|
||||
/** Never set on failure. */
|
||||
data?: never;
|
||||
/** What went wrong. */
|
||||
error: APIError;
|
||||
}
|
||||
|
||||
/** What every endpoint answers with, narrowed by `success`. */
|
||||
export type APIResponse<T> = APISuccess<T> | APIFailure;
|
||||
|
||||
/** One page of a list. */
|
||||
export interface PaginatedData<T> {
|
||||
/** The page's items. */
|
||||
items: T[];
|
||||
/** Whether another page follows. */
|
||||
hasMore: boolean;
|
||||
/** The whole list's size — only where it's shown (search results). */
|
||||
total?: number;
|
||||
|
||||
@@ -1,20 +1,42 @@
|
||||
/**
|
||||
* Categories, the admin-managed topics a dump can be filed under, and the
|
||||
* bodies that create and edit them.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** A category, which lists its dumps at `/<slug>`. */
|
||||
export interface Category {
|
||||
/** The category's id. */
|
||||
id: string;
|
||||
/** The category's URL slug, at the site root. */
|
||||
slug: string;
|
||||
/** The category's display name. */
|
||||
name: string;
|
||||
/** The category's place in the list, ascending. */
|
||||
position: number;
|
||||
/** When the category was created. */
|
||||
createdAt: Date;
|
||||
/** When the category was last changed, if ever. */
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
/** The body of `POST /api/categories`. */
|
||||
export interface CreateCategoryRequest {
|
||||
/** The new category's slug. */
|
||||
slug: string;
|
||||
/** The new category's display name. */
|
||||
name: string;
|
||||
/** Its place in the list; 0 when left out. */
|
||||
position?: number;
|
||||
}
|
||||
|
||||
/** The body of `PUT /api/categories/:id`: the fields to change. */
|
||||
export interface UpdateCategoryRequest {
|
||||
/** The new slug. */
|
||||
slug?: string;
|
||||
/** The new display name. */
|
||||
name?: string;
|
||||
/** The new place in the list. */
|
||||
position?: number;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
/**
|
||||
* A message of the site-wide chat.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** A chat message, with what the chat shows of its author and its reply. */
|
||||
export interface ChatMessage {
|
||||
/** The message's id. */
|
||||
id: string;
|
||||
/** The author's id. */
|
||||
userId: string;
|
||||
/** The message, in Markdown. */
|
||||
body: string;
|
||||
/** When the message was sent. */
|
||||
createdAt: Date;
|
||||
/** When the message was last edited, if ever. */
|
||||
updatedAt?: Date;
|
||||
/** The author's name. */
|
||||
authorUsername: string;
|
||||
/** The author's avatar type, undefined without one. */
|
||||
authorAvatarMime?: string;
|
||||
/** Id of the message this one replies to, if any. Retained even when the
|
||||
* target is later deleted (the preview fields below then go undefined). */
|
||||
/**
|
||||
* Id of the message this one replies to, if any. Retained even when the
|
||||
* target is later deleted (the preview fields below then go undefined).
|
||||
*/
|
||||
replyToId?: string;
|
||||
/** Author/snippet of the replied-to message, for a subtle inline reference.
|
||||
* Absent when there's no reply, or the target has since been deleted. */
|
||||
/**
|
||||
* Author of the replied-to message, for a subtle inline reference. Absent
|
||||
* when there's no reply, or the target has since been deleted.
|
||||
*/
|
||||
replyToAuthor?: string;
|
||||
/** Body of the replied-to message, absent under the same conditions. */
|
||||
replyToBody?: string;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,48 @@
|
||||
/**
|
||||
* Comments under a dump, and the bodies that post and edit them.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** A comment, with what the thread shows of its author. */
|
||||
export interface Comment {
|
||||
/** The comment's id. */
|
||||
id: string;
|
||||
/** The id of the dump it is posted under. */
|
||||
dumpId: string;
|
||||
/** The author's id. */
|
||||
userId: string;
|
||||
/** The id of the comment it replies to; undefined at the top level. */
|
||||
parentId?: string;
|
||||
/** The comment, in Markdown; empty once deleted. */
|
||||
body: string;
|
||||
/** When the comment was posted. */
|
||||
createdAt: Date;
|
||||
/** When the comment was last edited, if ever. */
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* Whether the comment was deleted: it stays in the thread, emptied, so its
|
||||
* replies keep their place.
|
||||
*/
|
||||
deleted: boolean;
|
||||
/** How many users like the comment. */
|
||||
likeCount: number;
|
||||
/** The author's name. */
|
||||
authorUsername: string;
|
||||
/** The author's avatar type, undefined without one. */
|
||||
authorAvatarMime?: string;
|
||||
}
|
||||
|
||||
/** The body of `POST /api/dumps/:dumpId/comments`. */
|
||||
export interface CreateCommentRequest {
|
||||
/** The comment, in Markdown. */
|
||||
body: string;
|
||||
/** The id of the comment it replies to, for a reply. */
|
||||
parentId?: string;
|
||||
}
|
||||
|
||||
/** The body of `PATCH /api/comments/:commentId`. */
|
||||
export interface UpdateCommentRequest {
|
||||
/** The new text, in Markdown. */
|
||||
body: string;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
/**
|
||||
* Dumps — the links and files users post — their link previews, and the bodies
|
||||
* that post and edit them.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** What a link's page says about itself, as its preview card shows it. */
|
||||
export interface RichContent {
|
||||
/**
|
||||
* Which provider read it: `youtube`, `bandcamp`, `soundcloud` or `generic`.
|
||||
*/
|
||||
type: string;
|
||||
/** The link the preview is of. */
|
||||
url: string;
|
||||
/** The site's name. */
|
||||
siteName?: string;
|
||||
/** The page's title. */
|
||||
title?: string;
|
||||
/** The page's description. */
|
||||
description?: string;
|
||||
/**
|
||||
* A real preview image (og:image, a large content image, a video still).
|
||||
@@ -14,28 +29,49 @@ export interface RichContent {
|
||||
faviconUrl?: string;
|
||||
/** The page's declared brand color, normalized to `#rrggbb`. */
|
||||
accentColor?: string;
|
||||
/** The YouTube video's id, for a YouTube link. */
|
||||
videoId?: string;
|
||||
/** The URL of the page's embeddable player, if it has one. */
|
||||
embedUrl?: string;
|
||||
}
|
||||
|
||||
/** A dump: a link or a file a user posted. */
|
||||
export interface Dump {
|
||||
/** The dump's id. */
|
||||
id: string;
|
||||
/** `url` for a link, `file` for an upload. */
|
||||
kind: "url" | "file";
|
||||
/** The dump's title. */
|
||||
title: string;
|
||||
/** The dump's URL slug. */
|
||||
slug?: string;
|
||||
/** The poster's description, in Markdown. */
|
||||
comment?: string;
|
||||
/** The poster's id. */
|
||||
userId: string;
|
||||
/** When the dump was posted. */
|
||||
createdAt: Date;
|
||||
/** When the dump was last edited, if ever. */
|
||||
updatedAt?: Date;
|
||||
/** The link, for a URL dump. */
|
||||
url?: string;
|
||||
/** The link's preview, for a URL dump. */
|
||||
richContent?: RichContent;
|
||||
/** The uploaded file's name, for a file dump. */
|
||||
fileName?: string;
|
||||
/** The uploaded file's type, for a file dump. */
|
||||
fileMime?: string;
|
||||
/** The uploaded file's size in bytes, for a file dump. */
|
||||
fileSize?: number;
|
||||
/** How many users upvoted the dump. */
|
||||
voteCount: number;
|
||||
/** How many comments it has, not counting deleted ones. */
|
||||
commentCount: number;
|
||||
/** Whether only the poster sees the dump. */
|
||||
isPrivate: boolean;
|
||||
/** The uploaded thumbnail's type, undefined without one. */
|
||||
thumbnailMime?: string;
|
||||
/** The ids of the categories it is filed under. */
|
||||
categoryIds?: string[];
|
||||
}
|
||||
|
||||
@@ -45,12 +81,19 @@ export interface Dump {
|
||||
* `findDumpsByUrl`.
|
||||
*/
|
||||
export interface DumpUrlMatch {
|
||||
/** The dump's id. */
|
||||
id: string;
|
||||
/** The dump's URL slug. */
|
||||
slug?: string;
|
||||
/** The dump's title. */
|
||||
title: string;
|
||||
/** The poster's name. */
|
||||
username: string;
|
||||
/** When the dump was posted. */
|
||||
createdAt: Date;
|
||||
/** How many users upvoted the dump. */
|
||||
voteCount: number;
|
||||
/** How many comments it has. */
|
||||
commentCount: number;
|
||||
}
|
||||
|
||||
@@ -60,29 +103,42 @@ export interface DumpUrlMatch {
|
||||
* page has no preview" from "this link is dead".
|
||||
*/
|
||||
export interface LinkPreview {
|
||||
/** Whether the page could be fetched. */
|
||||
reached: boolean;
|
||||
/** The preview, null when there is none at all. */
|
||||
richContent: RichContent | null;
|
||||
}
|
||||
|
||||
/** The JSON body of `POST /api/dumps`, which posts a link. */
|
||||
export interface CreateUrlDumpRequest {
|
||||
/** The link. */
|
||||
url: string;
|
||||
/** Overrides the title scraped from the page — the poster edited the preview. */
|
||||
title?: string;
|
||||
/** The poster's description, in Markdown. */
|
||||
comment?: string;
|
||||
/** Whether only the poster will see it. */
|
||||
isPrivate?: boolean;
|
||||
/** The ids of the categories to file it under. */
|
||||
categoryIds?: string[];
|
||||
}
|
||||
|
||||
/** The body of `PUT /api/dumps/:dumpId`: the fields to change. */
|
||||
export interface UpdateDumpRequest {
|
||||
/** The new link, for a URL dump. */
|
||||
url?: string;
|
||||
/** The new title. */
|
||||
title?: string;
|
||||
/** `null` removes the note; leaving it out keeps it. */
|
||||
comment?: string | null;
|
||||
/** Whether only the poster will see it. */
|
||||
isPrivate?: boolean;
|
||||
/** The ids of the categories to file it under, replacing the current ones. */
|
||||
categoryIds?: string[];
|
||||
}
|
||||
|
||||
/** `POST /api/attachments`: the id the uploaded image is served under. */
|
||||
export interface AttachmentResponse {
|
||||
/** The attachment's id, for `GET /api/attachments/:attachmentId`. */
|
||||
id: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
/**
|
||||
* What a user follows.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** `GET /api/follows/status`: the ids of what the caller follows. */
|
||||
export interface FollowStatus {
|
||||
/** The ids of the users they follow. */
|
||||
followedUserIds: string[];
|
||||
/** The ids of the playlists they follow. */
|
||||
followedPlaylistIds: string[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
// Matches @username not already inside a markdown link ([...] or (...)) and
|
||||
// not mid-word, so an email address doesn't mention its domain. The server
|
||||
// notifies whoever it matches; the chat highlights the same names.
|
||||
export const MENTION_RE = /(?<![[(\w])@(\w+)/g;
|
||||
/**
|
||||
* The @mention pattern, one for the server and the chat.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/**
|
||||
* Matches @username not already inside a markdown link ([...] or (...)) and
|
||||
* not mid-word, so an email address doesn't mention its domain; the name is
|
||||
* the first group. The server notifies whoever it matches; the chat highlights
|
||||
* the same names.
|
||||
*/
|
||||
export const MENTION_RE: RegExp = /(?<![[(\w])@(\w+)/g;
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Notifications: their types, and the data each type carries.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** What a notification is about. */
|
||||
export type NotificationType =
|
||||
| "playlist_followed"
|
||||
| "user_followed"
|
||||
@@ -8,64 +15,110 @@ export type NotificationType =
|
||||
| "dump_commented"
|
||||
| "comment_liked";
|
||||
|
||||
/** `playlist_followed`: someone followed one of the user's playlists. */
|
||||
export interface PlaylistFollowedData {
|
||||
/** The follower's id. */
|
||||
followerId: string;
|
||||
/** The follower's name. */
|
||||
followerUsername: string;
|
||||
/** The playlist's id. */
|
||||
playlistId: string;
|
||||
/** The playlist's title. */
|
||||
playlistTitle: string;
|
||||
}
|
||||
|
||||
/** `user_followed`: someone followed the user. */
|
||||
export interface UserFollowedData {
|
||||
/** The follower's id. */
|
||||
followerId: string;
|
||||
/** The follower's name. */
|
||||
followerUsername: string;
|
||||
}
|
||||
|
||||
/** `user_dump_posted`: a user they follow posted a dump. */
|
||||
export interface UserDumpPostedData {
|
||||
/** The poster's id. */
|
||||
dumperId: string;
|
||||
/** The poster's name. */
|
||||
dumperUsername: string;
|
||||
/** The dump's id. */
|
||||
dumpId: string;
|
||||
/** The dump's title. */
|
||||
dumpTitle: string;
|
||||
}
|
||||
|
||||
/** `playlist_dump_added`: a dump was added to a playlist they follow. */
|
||||
export interface PlaylistDumpAddedData {
|
||||
/** The dump's id. */
|
||||
dumpId: string;
|
||||
/** The dump's title. */
|
||||
dumpTitle: string;
|
||||
/** The playlist's id. */
|
||||
playlistId: string;
|
||||
/** The playlist's title. */
|
||||
playlistTitle: string;
|
||||
}
|
||||
|
||||
/** `dump_upvoted`: someone upvoted one of the user's dumps. */
|
||||
export interface DumpUpvotedData {
|
||||
/** The voter's id. */
|
||||
voterId: string;
|
||||
/** The voter's name. */
|
||||
voterUsername: string;
|
||||
/** The dump's id. */
|
||||
dumpId: string;
|
||||
/** The dump's title. */
|
||||
dumpTitle: string;
|
||||
}
|
||||
|
||||
/** `user_mentioned`: someone @mentioned the user. */
|
||||
export interface UserMentionedData {
|
||||
/** The mentioner's id. */
|
||||
mentionerId: string;
|
||||
/** The mentioner's name. */
|
||||
mentionerUsername: string;
|
||||
/** Where the mention was written. */
|
||||
contextType: "comment" | "dump" | "playlist" | "chat";
|
||||
/** The id of the comment, dump, playlist or chat message. */
|
||||
contextId: string;
|
||||
/**
|
||||
* The title of the dump or playlist, or of the dump a comment is under;
|
||||
* empty for the chat.
|
||||
*/
|
||||
contextTitle: string;
|
||||
/** The dump a comment is under, for a mention in a comment. */
|
||||
dumpId?: string;
|
||||
}
|
||||
|
||||
/** `dump_commented`: someone commented on one of the user's dumps. */
|
||||
export interface DumpCommentedData {
|
||||
/** The commenter's id. */
|
||||
commenterId: string;
|
||||
/** The commenter's name. */
|
||||
commenterUsername: string;
|
||||
/** The comment's id. */
|
||||
commentId: string;
|
||||
/** The dump's id. */
|
||||
dumpId: string;
|
||||
/** The dump's title. */
|
||||
dumpTitle: string;
|
||||
}
|
||||
|
||||
/** `comment_liked`: someone liked one of the user's comments. */
|
||||
export interface CommentLikedData {
|
||||
/** The liker's id. */
|
||||
likerId: string;
|
||||
/** The liker's name. */
|
||||
likerUsername: string;
|
||||
/** The comment's id. */
|
||||
commentId: string;
|
||||
/** The id of the dump it is under. */
|
||||
dumpId: string;
|
||||
/** The title of the dump it is under. */
|
||||
dumpTitle: string;
|
||||
}
|
||||
|
||||
/** The data of any notification; its {@link NotificationType} says which. */
|
||||
export type NotificationData =
|
||||
| PlaylistFollowedData
|
||||
| UserFollowedData
|
||||
@@ -76,11 +129,18 @@ export type NotificationData =
|
||||
| DumpCommentedData
|
||||
| CommentLikedData;
|
||||
|
||||
/** A notification sent to a user. */
|
||||
export interface Notification {
|
||||
/** The notification's id. */
|
||||
id: string;
|
||||
/** The id of the user it is for. */
|
||||
userId: string;
|
||||
/** What it is about. */
|
||||
type: NotificationType;
|
||||
/** Its details, shaped by `type`. */
|
||||
data: NotificationData;
|
||||
/** Whether the user has read it. */
|
||||
read: boolean;
|
||||
/** When it was sent. */
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
/**
|
||||
* The role permissions, one table for the server that enforces them and the
|
||||
* client that hides what a role can't do.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { Role } from "./user.ts";
|
||||
|
||||
// Named capabilities checked at enforcement points. Extend this union as new
|
||||
// gated capabilities are added (e.g. "invite:manage").
|
||||
/**
|
||||
* Named capabilities checked at enforcement points. Extend this union as new
|
||||
* gated capabilities are added (e.g. "invite:manage").
|
||||
*/
|
||||
export type Permission =
|
||||
| "dump:moderate"
|
||||
| "comment:moderate"
|
||||
@@ -10,6 +19,7 @@ export type Permission =
|
||||
| "user:manage"
|
||||
| "category:manage";
|
||||
|
||||
/** Every {@link Permission}. */
|
||||
const ALL_PERMISSIONS: readonly Permission[] = [
|
||||
"dump:moderate",
|
||||
"comment:moderate",
|
||||
@@ -19,8 +29,11 @@ const ALL_PERMISSIONS: readonly Permission[] = [
|
||||
"category:manage",
|
||||
];
|
||||
|
||||
// Roles are fixed in code. `admin` is a superset of every permission (current
|
||||
// and future); `moderator` gets the content-moderation permissions only.
|
||||
/**
|
||||
* What each role may do. Roles are fixed in code. `admin` is a superset of
|
||||
* every permission (current and future); `moderator` gets the
|
||||
* content-moderation permissions only.
|
||||
*/
|
||||
export const ROLE_PERMISSIONS: Record<Role, readonly Permission[]> = {
|
||||
admin: ALL_PERMISSIONS,
|
||||
moderator: [
|
||||
@@ -32,6 +45,7 @@ export const ROLE_PERMISSIONS: Record<Role, readonly Permission[]> = {
|
||||
user: [],
|
||||
};
|
||||
|
||||
/** Checks whether `role` grants `permission`. */
|
||||
export function hasPermission(role: Role, permission: Permission): boolean {
|
||||
return ROLE_PERMISSIONS[role].includes(permission);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,68 @@
|
||||
/** `GET /api/bandcamp/tracks`: a Bandcamp page resolved at play time. */
|
||||
/**
|
||||
* What the player's resolvers answer: a Bandcamp release's streams and a
|
||||
* YouTube playlist's tracks.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** A track of a Bandcamp release, as {@link Tralbum} lists it. */
|
||||
export interface TralbumTrack {
|
||||
/** Position in the parsed array — the stable handle used to re-resolve a
|
||||
* track after its signed URL expires. */
|
||||
/**
|
||||
* Position in the parsed array — the stable handle used to re-resolve a
|
||||
* track after its signed URL expires.
|
||||
*/
|
||||
index: number;
|
||||
/** The track's number on the release. */
|
||||
trackNum?: number;
|
||||
/** The track's title. */
|
||||
title: string;
|
||||
/** Seconds, fractional. */
|
||||
duration?: number;
|
||||
/** The signed stream URL, null when the track can't be played. */
|
||||
streamUrl: string | null;
|
||||
/** False for preorder, unreleased and still-encoding tracks. They stay in
|
||||
* the list so numbering matches the release, but never enter the queue. */
|
||||
/**
|
||||
* False for preorder, unreleased and still-encoding tracks. They stay in
|
||||
* the list so numbering matches the release, but never enter the queue.
|
||||
*/
|
||||
streamable: boolean;
|
||||
/** Bandcamp caps free plays per track; a capped track may serve a clip. */
|
||||
capped: boolean;
|
||||
}
|
||||
|
||||
/** `GET /api/bandcamp/tracks`: a Bandcamp page resolved at play time. */
|
||||
export interface Tralbum {
|
||||
/** The page it was resolved from. */
|
||||
sourceUrl: string;
|
||||
/** Whether the page is an album or a single track. */
|
||||
itemType: "album" | "track";
|
||||
/** The release's title. */
|
||||
title?: string;
|
||||
/** The artist's name. */
|
||||
artist?: string;
|
||||
/** The release's cover. */
|
||||
artworkUrl?: string;
|
||||
/** The release's tracks, in order. */
|
||||
tracks: TralbumTrack[];
|
||||
/** Epoch ms of the fetch that produced these signed URLs. */
|
||||
resolvedAt: number;
|
||||
}
|
||||
|
||||
/** `GET /api/youtube/playlist`: a playlist's tracklist, for the player. */
|
||||
/** A video of a YouTube playlist, as {@link YouTubePlaylist} lists it. */
|
||||
export interface YouTubePlaylistTrack {
|
||||
/** The video's id. */
|
||||
videoId: string;
|
||||
/** The video's title. */
|
||||
title: string;
|
||||
/** Seconds. */
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/** `GET /api/youtube/playlist`: a playlist's tracklist, for the player. */
|
||||
export interface YouTubePlaylist {
|
||||
/** The playlist's id. */
|
||||
listId: string;
|
||||
/** The playlist's title. */
|
||||
title?: string;
|
||||
/** Its videos, in order. */
|
||||
tracks: YouTubePlaylistTrack[];
|
||||
}
|
||||
|
||||
@@ -1,40 +1,80 @@
|
||||
/**
|
||||
* Playlists — ordered collections of dumps — and the bodies that create, edit
|
||||
* and reorder them.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { Dump } from "./dump.ts";
|
||||
|
||||
/** A playlist. */
|
||||
export interface Playlist {
|
||||
/** The playlist's id. */
|
||||
id: string;
|
||||
/** The owner's id. */
|
||||
userId: string;
|
||||
/** The playlist's title. */
|
||||
title: string;
|
||||
/** The playlist's URL slug. */
|
||||
slug?: string;
|
||||
/** The owner's description, in Markdown. */
|
||||
description?: string;
|
||||
/** Whether anyone may see it, or only its owner. */
|
||||
isPublic: boolean;
|
||||
/** When the playlist was created. */
|
||||
createdAt: Date;
|
||||
/** When the playlist was last changed, if ever. */
|
||||
updatedAt?: Date;
|
||||
/** The cover image's type, undefined without one. */
|
||||
imageMime?: string;
|
||||
/**
|
||||
* How many dumps it holds, private ones included, where the query counts
|
||||
* them.
|
||||
*/
|
||||
dumpCount?: number;
|
||||
/** The owner's name, where the query joins it. */
|
||||
ownerUsername?: string;
|
||||
}
|
||||
|
||||
/** A playlist with the dumps the caller may see in it, in order. */
|
||||
export interface PlaylistWithDumps extends Playlist {
|
||||
/** The dumps, in the playlist's order. */
|
||||
dumps: Dump[];
|
||||
}
|
||||
|
||||
/**
|
||||
* `GET /api/playlists/by-dump/:dumpId/memberships`: one of the caller's
|
||||
* playlists, and whether it holds the dump.
|
||||
*/
|
||||
export interface PlaylistMembership {
|
||||
/** The playlist. */
|
||||
playlist: Playlist;
|
||||
/** Whether it holds the dump. */
|
||||
hasDump: boolean;
|
||||
}
|
||||
|
||||
/** The body of `POST /api/playlists`. */
|
||||
export interface CreatePlaylistRequest {
|
||||
/** The new playlist's title. */
|
||||
title: string;
|
||||
/** Its description, in Markdown. */
|
||||
description?: string;
|
||||
/** Whether anyone may see it. */
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
/** The body of `PUT /api/playlists/:playlistId`: the fields to change. */
|
||||
export interface UpdatePlaylistRequest {
|
||||
/** The new title. */
|
||||
title?: string;
|
||||
/** The new description; `null` removes it. */
|
||||
description?: string | null;
|
||||
/** Whether anyone may see it. */
|
||||
isPublic?: boolean;
|
||||
}
|
||||
|
||||
/** The body of `PUT /api/playlists/:playlistId/order`. */
|
||||
export interface ReorderPlaylistRequest {
|
||||
/** Every dump of the playlist the caller can see, in the new order. */
|
||||
dumpIds: string[];
|
||||
}
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
/**
|
||||
* What a search answers.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { PaginatedData } from "./api.ts";
|
||||
import type { Dump } from "./dump.ts";
|
||||
import type { Playlist } from "./playlist.ts";
|
||||
import type { PublicUser } from "./user.ts";
|
||||
|
||||
/** `GET /api/search`: what matches the query, in each kind. */
|
||||
export interface SearchResults {
|
||||
/** A page of the matching dumps, with their total. */
|
||||
dumps: PaginatedData<Dump>;
|
||||
/** The matching users. */
|
||||
users: PublicUser[];
|
||||
/** The matching playlists. */
|
||||
playlists: Playlist[];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
/**
|
||||
* Users as the API shows them, their roles, and the bodies and answers of
|
||||
* sign-up, sign-in, profile edits and invites.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** What a user may do beyond the basics; see `ROLE_PERMISSIONS`. */
|
||||
export type Role = "user" | "moderator" | "admin";
|
||||
|
||||
/** Every {@link Role}, least to most privileged. */
|
||||
export const ROLES: readonly Role[] = ["user", "moderator", "admin"];
|
||||
|
||||
/** Checks whether `value` is a {@link Role}. */
|
||||
export function isRole(value: unknown): value is Role {
|
||||
return typeof value === "string" &&
|
||||
(ROLES as readonly string[]).includes(value);
|
||||
@@ -9,69 +19,111 @@ export function isRole(value: unknown): value is Role {
|
||||
|
||||
/** A user as anyone may see them: no password hash, no email. */
|
||||
export interface PublicUser {
|
||||
/** The user's id. */
|
||||
id: string;
|
||||
/** The user's name, fixed at sign-up. */
|
||||
username: string;
|
||||
/** The user's role. */
|
||||
role: Role;
|
||||
/** When the user signed up. */
|
||||
createdAt: Date;
|
||||
/** When the user last changed, if ever. */
|
||||
updatedAt?: Date;
|
||||
/** The avatar's type, undefined without one. */
|
||||
avatarMime?: string;
|
||||
/** The user's description of themselves, in Markdown. */
|
||||
description?: string;
|
||||
/** The name of whoever invited them. */
|
||||
invitedByUsername?: string;
|
||||
}
|
||||
|
||||
/** A user as they see themselves: everything but the password hash. */
|
||||
export interface SelfUser extends PublicUser {
|
||||
/** The address password resets are sent to. */
|
||||
email: string;
|
||||
}
|
||||
|
||||
/** A match for the @mention autocomplete. */
|
||||
export interface UserSuggestion {
|
||||
/** The user's id. */
|
||||
id: string;
|
||||
/** The user's name. */
|
||||
username: string;
|
||||
/** The avatar's type, null without one. */
|
||||
avatarMime: string | null;
|
||||
}
|
||||
|
||||
/** Someone a user invited, directly or down the line. */
|
||||
export interface Invitee {
|
||||
/** The user's id. */
|
||||
id: string;
|
||||
/** The user's name. */
|
||||
username: string;
|
||||
/** The avatar's type, undefined without one. */
|
||||
avatarMime?: string;
|
||||
/** The id of whoever invited them, to build the tree. */
|
||||
invitedById: string;
|
||||
/** When they signed up. */
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
/** The body of `POST /api/users/login`. */
|
||||
export interface LoginUserRequest {
|
||||
/** The user's name. */
|
||||
username: string;
|
||||
/** The user's password. */
|
||||
password: string;
|
||||
}
|
||||
|
||||
/** The body of `POST /api/users/register`. */
|
||||
export interface RegisterUserRequest {
|
||||
/** The name to sign up with. */
|
||||
username: string;
|
||||
/** The password to sign up with. */
|
||||
password: string;
|
||||
/** The invite's token. */
|
||||
inviteToken: string;
|
||||
/** The address password resets will be sent to. */
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fields of a user to change. `PATCH /api/users/me` takes only `description`
|
||||
* and `email` from it; the role and the password change through their own
|
||||
* endpoints.
|
||||
*/
|
||||
export interface UpdateUserRequest {
|
||||
/** The new name. */
|
||||
username?: string;
|
||||
/** The new password. */
|
||||
password?: string;
|
||||
/** The new role. */
|
||||
role?: Role;
|
||||
/** The new description; `null` removes it. */
|
||||
description?: string | null;
|
||||
/** The new email address. */
|
||||
email?: string;
|
||||
}
|
||||
|
||||
/** The session token, plus the media-only one for file and thumbnail URLs. */
|
||||
export interface AuthTokens {
|
||||
/** The session token, sent as a bearer token. */
|
||||
token: string;
|
||||
/** The token media URLs carry as `?token=`, good for nothing else. */
|
||||
mediaToken: string;
|
||||
}
|
||||
|
||||
/** What sign-up and sign-in answer: the user and their tokens. */
|
||||
export interface AuthResponse extends AuthTokens {
|
||||
/** The signed-in user. */
|
||||
user: SelfUser;
|
||||
}
|
||||
|
||||
/** `GET /api/users/me/media-token`: a fresh media token. */
|
||||
export type MediaTokenResponse = Pick<AuthTokens, "mediaToken">;
|
||||
|
||||
/** `POST /api/invites`: the new invite's token. */
|
||||
export interface InviteResponse {
|
||||
/** The token, which the invite link carries. */
|
||||
token: string;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,39 @@
|
||||
/**
|
||||
* The limits on what users submit, one set for the server that enforces them
|
||||
* and the forms that check them before sending.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** Limits the server enforces and the forms check before sending. */
|
||||
export const VALIDATION = {
|
||||
/** Shortest username, in characters. */
|
||||
USERNAME_MIN: 1,
|
||||
/** Longest username. */
|
||||
USERNAME_MAX: 32,
|
||||
/** Shortest password. */
|
||||
PASSWORD_MIN: 8,
|
||||
/** Longest password. */
|
||||
PASSWORD_MAX: 128,
|
||||
/** Longest dump title. */
|
||||
DUMP_TITLE_MAX: 200,
|
||||
/** Longest dump description. */
|
||||
DUMP_COMMENT_MAX: 5000,
|
||||
/** Longest playlist title. */
|
||||
PLAYLIST_TITLE_MAX: 100,
|
||||
/** Longest playlist description. */
|
||||
PLAYLIST_DESCRIPTION_MAX: 2000,
|
||||
/** Longest comment. */
|
||||
COMMENT_BODY_MAX: 5000,
|
||||
/** Longest profile description. */
|
||||
USER_DESCRIPTION_MAX: 2000,
|
||||
/** Longest category name. */
|
||||
CATEGORY_NAME_MAX: 50,
|
||||
/** Longest category slug. */
|
||||
CATEGORY_SLUG_MAX: 50,
|
||||
/** Longest chat message. */
|
||||
CHAT_MESSAGE_MAX: 2000,
|
||||
} as const;
|
||||
|
||||
/** Largest file a file dump may hold. */
|
||||
export const DUMP_MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB
|
||||
/** Largest file a file dump may hold, in bytes (50 MB). */
|
||||
export const DUMP_MAX_FILE_SIZE_BYTES: number = 50 * 1024 * 1024;
|
||||
|
||||
190
shared/ws.ts
190
shared/ws.ts
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* The socket protocol. Every message has a `type` discriminant, so each union
|
||||
* narrows with a switch. The server sends these as JSON, so the client reads
|
||||
* `Wire<ServerToClientMessage>`: its Dates arrive as strings.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { ChatMessage } from "./chat.ts";
|
||||
import type { Comment } from "./comment.ts";
|
||||
import type { Dump } from "./dump.ts";
|
||||
@@ -5,83 +13,117 @@ import type { Notification } from "./notification.ts";
|
||||
import type { Playlist } from "./playlist.ts";
|
||||
import type { PublicUser } from "./user.ts";
|
||||
|
||||
/**
|
||||
* The socket protocol. Every message has a `type` discriminant, so each union
|
||||
* narrows with a switch. The server sends these as JSON, so the client reads
|
||||
* `Wire<ServerToClientMessage>`: its Dates arrive as strings.
|
||||
*/
|
||||
|
||||
// ── Client → Server ──────────────────────────────────────────────────────────
|
||||
|
||||
/** A keepalive; the other side answers {@link PongMessage}. Both directions. */
|
||||
export interface PingMessage {
|
||||
/** The discriminant. */
|
||||
type: "ping";
|
||||
}
|
||||
|
||||
/** The answer to a {@link PingMessage}. Both directions. */
|
||||
export interface PongMessage {
|
||||
/** The discriminant. */
|
||||
type: "pong";
|
||||
}
|
||||
|
||||
/** Upvotes a dump; answered by {@link VoteAckMessage}. */
|
||||
export interface VoteCastMessage {
|
||||
/** The discriminant. */
|
||||
type: "vote_cast";
|
||||
/** The dump's id. */
|
||||
dumpId: string;
|
||||
}
|
||||
|
||||
/** Withdraws an upvote; answered by {@link VoteAckMessage}. */
|
||||
export interface VoteRemoveMessage {
|
||||
/** The discriminant. */
|
||||
type: "vote_remove";
|
||||
/** The dump's id. */
|
||||
dumpId: string;
|
||||
}
|
||||
|
||||
/** Likes a comment; answered by {@link CommentLikeAckMessage}. */
|
||||
export interface CommentLikeCastMessage {
|
||||
/** The discriminant. */
|
||||
type: "comment_like_cast";
|
||||
/** The comment's id. */
|
||||
commentId: string;
|
||||
}
|
||||
|
||||
/** Withdraws a like; answered by {@link CommentLikeAckMessage}. */
|
||||
export interface CommentLikeRemoveMessage {
|
||||
/** The discriminant. */
|
||||
type: "comment_like_remove";
|
||||
/** The comment's id. */
|
||||
commentId: string;
|
||||
}
|
||||
|
||||
/** Posts a chat message; broadcast back as {@link ChatMessageMessage}. */
|
||||
export interface ChatSendMessage {
|
||||
/** The discriminant. */
|
||||
type: "chat_send";
|
||||
/** The message, in Markdown. */
|
||||
body: string;
|
||||
/** Id of the message being replied to, if this is a reply. */
|
||||
replyToId?: string;
|
||||
/** Client-chosen id echoed on the broadcast (or on the error), so the
|
||||
* sender can match the outcome to its pending message. */
|
||||
/**
|
||||
* Client-chosen id echoed on the broadcast (or on the error), so the
|
||||
* sender can match the outcome to its pending message.
|
||||
*/
|
||||
nonce?: string;
|
||||
}
|
||||
|
||||
// Sent while the user is composing (true) and when they stop (false). The
|
||||
// server rebroadcasts the live set of typers to everyone as ChatTypingUpdate.
|
||||
/**
|
||||
* Sent while the user is composing (true) and when they stop (false). The
|
||||
* server rebroadcasts the live set of typers to everyone as
|
||||
* {@link ChatTypingUpdateMessage}.
|
||||
*/
|
||||
export interface ChatTypingMessage {
|
||||
/** The discriminant. */
|
||||
type: "chat_typing";
|
||||
/** Whether the user is composing. */
|
||||
typing: boolean;
|
||||
}
|
||||
|
||||
// Tells the server whether this client currently has the chatbox open, so that
|
||||
// mention notifications can be skipped for users who are already reading chat.
|
||||
/**
|
||||
* Tells the server whether this client currently has the chatbox open, so that
|
||||
* mention notifications can be skipped for users who are already reading chat.
|
||||
*/
|
||||
export interface ChatFocusMessage {
|
||||
/** The discriminant. */
|
||||
type: "chat_focus";
|
||||
/** Whether the chatbox is open. */
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
/** Edits a chat message: the user's own, or any with `chat:moderate`. */
|
||||
export interface ChatEditMessage {
|
||||
/** The discriminant. */
|
||||
type: "chat_edit";
|
||||
/** The message's id. */
|
||||
id: string;
|
||||
/** The new text, in Markdown. */
|
||||
body: string;
|
||||
}
|
||||
|
||||
/** Deletes a chat message: the user's own, or any with `chat:moderate`. */
|
||||
export interface ChatDeleteMessage {
|
||||
/** The discriminant. */
|
||||
type: "chat_delete";
|
||||
/** The message's id. */
|
||||
id: string;
|
||||
}
|
||||
|
||||
// The user has seen the chat up to the message created at `at`.
|
||||
/** The user has seen the chat up to the message created at `at`. */
|
||||
export interface ChatReadMessage {
|
||||
/** The discriminant. */
|
||||
type: "chat_read";
|
||||
/** The creation time of the last message seen, as an ISO string. */
|
||||
at: string;
|
||||
}
|
||||
|
||||
/** Any message a client sends. */
|
||||
export type ClientToServerMessage =
|
||||
| PingMessage
|
||||
| PongMessage
|
||||
@@ -98,93 +140,156 @@ export type ClientToServerMessage =
|
||||
|
||||
// ── Server → Client ──────────────────────────────────────────────────────────
|
||||
|
||||
/** A signed-in user with at least one socket open. */
|
||||
export interface OnlineUser {
|
||||
/** The user's id. */
|
||||
userId: string;
|
||||
/** The user's name. */
|
||||
username: string;
|
||||
/** Whether the user has an avatar. */
|
||||
hasAvatar: boolean;
|
||||
/** When the avatar last changed, to bust the client's cache. */
|
||||
avatarVersion?: number;
|
||||
}
|
||||
|
||||
/** The first message on a new socket: the state the client starts from. */
|
||||
export interface WelcomeMessage {
|
||||
/** The discriminant. */
|
||||
type: "welcome";
|
||||
/** Who is online. */
|
||||
users: OnlineUser[];
|
||||
/** The ids of the dumps the user upvoted; empty when signed out. */
|
||||
myVotes: string[];
|
||||
/** The ids of the comments the user likes; empty when signed out. */
|
||||
myCommentLikes: string[];
|
||||
/** How many notifications the user hasn't read. */
|
||||
unreadNotificationCount: number;
|
||||
/** The user's chat read marker and the unread count behind it (signed-in
|
||||
* users only). */
|
||||
/**
|
||||
* The user's chat read marker (signed-in users only), as an ISO string.
|
||||
*/
|
||||
chatReadAt?: string;
|
||||
/** How many chat messages came after the read marker. */
|
||||
unreadChatCount: number;
|
||||
}
|
||||
|
||||
/** Who is online, sent to everyone whenever it changes. */
|
||||
export interface PresenceUpdateMessage {
|
||||
/** The discriminant. */
|
||||
type: "presence_update";
|
||||
/** Who is online. */
|
||||
users: OnlineUser[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A dump's vote count changed; sent to everyone, or only to the poster for a
|
||||
* private dump.
|
||||
*/
|
||||
export interface VotesUpdateMessage {
|
||||
/** The discriminant. */
|
||||
type: "votes_update";
|
||||
/** The dump's id. */
|
||||
dumpId: string;
|
||||
/** Its new vote count. */
|
||||
voteCount: number;
|
||||
/** Who voted. */
|
||||
voterId: string;
|
||||
/** Whether the vote was cast or withdrawn. */
|
||||
action: "cast" | "remove";
|
||||
}
|
||||
|
||||
/** Confirms the sender's own vote, with the count it led to. */
|
||||
export interface VoteAckMessage {
|
||||
/** The discriminant. */
|
||||
type: "vote_ack";
|
||||
/** The dump's id. */
|
||||
dumpId: string;
|
||||
/** Whether the vote was cast or withdrawn. */
|
||||
action: "cast" | "remove";
|
||||
/** The dump's new vote count. */
|
||||
voteCount: number;
|
||||
}
|
||||
|
||||
/** A comment's like count changed. */
|
||||
export interface CommentLikesUpdateMessage {
|
||||
/** The discriminant. */
|
||||
type: "comment_likes_update";
|
||||
/** The comment's id. */
|
||||
commentId: string;
|
||||
/** Its new like count. */
|
||||
likeCount: number;
|
||||
/** Who liked it. */
|
||||
likerId: string;
|
||||
/** Whether the like was cast or withdrawn. */
|
||||
action: "cast" | "remove";
|
||||
}
|
||||
|
||||
/** Confirms the sender's own like, with the count it led to. */
|
||||
export interface CommentLikeAckMessage {
|
||||
/** The discriminant. */
|
||||
type: "comment_like_ack";
|
||||
/** The comment's id. */
|
||||
commentId: string;
|
||||
/** Whether the like was cast or withdrawn. */
|
||||
action: "cast" | "remove";
|
||||
/** The comment's new like count. */
|
||||
likeCount: number;
|
||||
}
|
||||
|
||||
/** A dump was posted. */
|
||||
export interface DumpCreatedMessage {
|
||||
/** The discriminant. */
|
||||
type: "dump_created";
|
||||
/** The new dump. */
|
||||
dump: Dump;
|
||||
}
|
||||
|
||||
/** A dump changed. */
|
||||
export interface DumpUpdatedMessage {
|
||||
/** The discriminant. */
|
||||
type: "dump_updated";
|
||||
/** The dump as it is now. */
|
||||
dump: Dump;
|
||||
}
|
||||
|
||||
/** A dump was deleted. */
|
||||
export interface DumpDeletedMessage {
|
||||
/** The discriminant. */
|
||||
type: "dump_deleted";
|
||||
/** The deleted dump's id. */
|
||||
dumpId: string;
|
||||
}
|
||||
|
||||
/** A playlist was created; sent to whoever may see it. */
|
||||
export interface PlaylistCreatedMessage {
|
||||
/** The discriminant. */
|
||||
type: "playlist_created";
|
||||
/** The new playlist. */
|
||||
playlist: Playlist;
|
||||
}
|
||||
|
||||
/** A playlist changed; sent to whoever may see it, or could before. */
|
||||
export interface PlaylistUpdatedMessage {
|
||||
/** The discriminant. */
|
||||
type: "playlist_updated";
|
||||
/** The playlist as it is now. */
|
||||
playlist: Playlist;
|
||||
}
|
||||
|
||||
/** A playlist was deleted; sent to whoever could see it. */
|
||||
export interface PlaylistDeletedMessage {
|
||||
/** The discriminant. */
|
||||
type: "playlist_deleted";
|
||||
/** The deleted playlist's id. */
|
||||
playlistId: string;
|
||||
/** Its owner's id. */
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/** A playlist's dumps changed; each recipient gets the ones they may see. */
|
||||
export interface PlaylistDumpsUpdatedMessage {
|
||||
/** The discriminant. */
|
||||
type: "playlist_dumps_updated";
|
||||
/** The playlist's id. */
|
||||
playlistId: string;
|
||||
/** The members this recipient can see, in order. */
|
||||
dumpIds: string[];
|
||||
@@ -192,39 +297,64 @@ export interface PlaylistDumpsUpdatedMessage {
|
||||
dumpCount: number;
|
||||
}
|
||||
|
||||
/** A user's public profile changed. */
|
||||
export interface UserUpdatedMessage {
|
||||
/** The discriminant. */
|
||||
type: "user_updated";
|
||||
/** The user as they are now. */
|
||||
user: PublicUser;
|
||||
}
|
||||
|
||||
/** A comment was posted. */
|
||||
export interface CommentCreatedMessage {
|
||||
/** The discriminant. */
|
||||
type: "comment_created";
|
||||
/** The new comment. */
|
||||
comment: Comment;
|
||||
}
|
||||
|
||||
/** A comment was edited. */
|
||||
export interface CommentUpdatedMessage {
|
||||
/** The discriminant. */
|
||||
type: "comment_updated";
|
||||
/** The comment as it is now. */
|
||||
comment: Comment;
|
||||
}
|
||||
|
||||
/** A comment was deleted. */
|
||||
export interface CommentDeletedMessage {
|
||||
/** The discriminant. */
|
||||
type: "comment_deleted";
|
||||
/** The deleted comment's id. */
|
||||
commentId: string;
|
||||
/** The id of the dump it was under. */
|
||||
dumpId: string;
|
||||
}
|
||||
|
||||
/** A notification for the recipient. */
|
||||
export interface NotificationCreatedMessage {
|
||||
/** The discriminant. */
|
||||
type: "notification_created";
|
||||
/** The notification. */
|
||||
notification: Notification;
|
||||
}
|
||||
|
||||
/** An action the client sent failed. */
|
||||
export interface ErrorMessage {
|
||||
/** The discriminant. */
|
||||
type: "error";
|
||||
/** What went wrong. */
|
||||
message?: string;
|
||||
// Context for optimistic-action failures so the client can revert the exact
|
||||
// pending vote/like immediately instead of waiting for its ACK timeout.
|
||||
/**
|
||||
* The dump of a failed vote. With `commentId` and `action`, it gives the
|
||||
* client the context of an optimistic-action failure, so it can revert the
|
||||
* exact pending vote or like immediately instead of waiting for its ACK
|
||||
* timeout.
|
||||
*/
|
||||
dumpId?: string;
|
||||
/** The comment of a failed like. */
|
||||
commentId?: string;
|
||||
/** The failed vote's or like's action. */
|
||||
action?: "cast" | "remove";
|
||||
/** Set when a chat action failed, so the client can surface it in the chat. */
|
||||
scope?: "chat";
|
||||
@@ -232,41 +362,61 @@ export interface ErrorMessage {
|
||||
nonce?: string;
|
||||
}
|
||||
|
||||
/** The session ended — the account was deleted or its token revoked. */
|
||||
export interface ForceLogoutMessage {
|
||||
/** The discriminant. */
|
||||
type: "force_logout";
|
||||
}
|
||||
|
||||
/** A chat message was posted. */
|
||||
export interface ChatMessageMessage {
|
||||
/** The discriminant. */
|
||||
type: "chat_message";
|
||||
/** The new message. */
|
||||
message: ChatMessage;
|
||||
/** The sender's nonce, so its pending copy can be retired. */
|
||||
nonce?: string;
|
||||
}
|
||||
|
||||
/** A chat message was edited. */
|
||||
export interface ChatMessageUpdatedMessage {
|
||||
/** The discriminant. */
|
||||
type: "chat_message_updated";
|
||||
/** The message as it is now. */
|
||||
message: ChatMessage;
|
||||
}
|
||||
|
||||
/** A chat message was deleted. */
|
||||
export interface ChatMessageDeletedMessage {
|
||||
/** The discriminant. */
|
||||
type: "chat_message_deleted";
|
||||
/** The deleted message's id. */
|
||||
id: string;
|
||||
}
|
||||
|
||||
// The current set of users composing a chat message. Reuses OnlineUser so the
|
||||
// client can render the same avatars as the presence row.
|
||||
/**
|
||||
* The current set of users composing a chat message. Reuses
|
||||
* {@link OnlineUser} so the client can render the same avatars as the
|
||||
* presence row.
|
||||
*/
|
||||
export interface ChatTypingUpdateMessage {
|
||||
/** The discriminant. */
|
||||
type: "chat_typing_update";
|
||||
/** Who is composing. */
|
||||
users: OnlineUser[];
|
||||
}
|
||||
|
||||
// The user's read marker moved (from any of their tabs or devices).
|
||||
/** The user's read marker moved (from any of their tabs or devices). */
|
||||
export interface ChatReadUpdateMessage {
|
||||
/** The discriminant. */
|
||||
type: "chat_read_update";
|
||||
/** The new marker, as an ISO string. */
|
||||
readAt: string;
|
||||
/** How many messages came after it. */
|
||||
unread: number;
|
||||
}
|
||||
|
||||
/** Any message the server sends. */
|
||||
export type ServerToClientMessage =
|
||||
| PingMessage
|
||||
| PongMessage
|
||||
|
||||
Reference in New Issue
Block a user