467 lines
9.7 KiB
TypeScript
467 lines
9.7 KiB
TypeScript
/**
|
|
* Backend
|
|
*/
|
|
|
|
export interface RichContent {
|
|
type: string;
|
|
url: string;
|
|
siteName?: string;
|
|
title?: string;
|
|
description?: string;
|
|
thumbnailUrl?: string;
|
|
videoId?: string;
|
|
embedUrl?: string;
|
|
}
|
|
|
|
export interface Dump {
|
|
id: string;
|
|
kind: "url" | "file";
|
|
title: string;
|
|
slug?: string;
|
|
comment?: string;
|
|
userId: string;
|
|
createdAt: Date;
|
|
updatedAt?: Date;
|
|
url?: string;
|
|
richContent?: RichContent;
|
|
fileName?: string;
|
|
fileMime?: string;
|
|
fileSize?: number;
|
|
voteCount: number;
|
|
commentCount: number;
|
|
isPrivate: boolean;
|
|
}
|
|
|
|
/**
|
|
* Authentication
|
|
*/
|
|
|
|
export interface User {
|
|
id: string;
|
|
username: string;
|
|
passwordHash: string;
|
|
isAdmin: boolean;
|
|
createdAt: Date;
|
|
updatedAt?: Date;
|
|
avatarMime?: string;
|
|
invitedByUsername?: string;
|
|
}
|
|
|
|
export interface LoginUserRequest {
|
|
username: string;
|
|
password: string;
|
|
}
|
|
|
|
export interface RegisterUserRequest {
|
|
username: string;
|
|
password: string;
|
|
inviteToken: string;
|
|
}
|
|
|
|
export interface UpdateUserRequest {
|
|
username?: string;
|
|
password?: string;
|
|
isAdmin?: boolean;
|
|
}
|
|
|
|
export function isLoginUserRequest(obj: unknown): obj is LoginUserRequest {
|
|
return !!obj && typeof obj === "object" &&
|
|
"username" in obj && typeof obj.username === "string" &&
|
|
"password" in obj && typeof obj.password === "string";
|
|
}
|
|
|
|
export function isRegisterUserRequest(
|
|
obj: unknown,
|
|
): obj is RegisterUserRequest {
|
|
return !!obj && typeof obj === "object" &&
|
|
"username" in obj && typeof obj.username === "string" &&
|
|
"password" in obj && typeof obj.password === "string" &&
|
|
"inviteToken" in obj && typeof obj.inviteToken === "string";
|
|
}
|
|
|
|
export function isUpdateUserRequest(obj: unknown): obj is UpdateUserRequest {
|
|
return !!obj && typeof obj === "object" &&
|
|
(!("username" in obj) || typeof obj.username === "string") &&
|
|
(!("password" in obj) || typeof obj.password === "string") &&
|
|
(!("isAdmin" in obj) || typeof obj.isAdmin === "boolean");
|
|
}
|
|
|
|
export interface AuthResponse {
|
|
token: string;
|
|
user: User;
|
|
}
|
|
|
|
export interface AuthPayload {
|
|
userId: string;
|
|
username: string;
|
|
isAdmin: boolean;
|
|
exp: number;
|
|
}
|
|
|
|
export function isAuthPayload(obj: unknown): obj is AuthPayload {
|
|
return !!obj &&
|
|
typeof obj === "object" &&
|
|
"userId" in obj && typeof obj.userId === "string" &&
|
|
"username" in obj && typeof obj.username === "string" &&
|
|
"isAdmin" in obj && typeof obj.isAdmin === "boolean" &&
|
|
"exp" in obj && typeof obj.exp === "number";
|
|
}
|
|
|
|
export interface InvitePayload {
|
|
purpose: "invite";
|
|
inviterId: string;
|
|
exp: number;
|
|
}
|
|
|
|
export function isInvitePayload(obj: unknown): obj is InvitePayload {
|
|
return !!obj && typeof obj === "object" &&
|
|
"purpose" in obj && (obj as Record<string, unknown>).purpose === "invite" &&
|
|
"inviterId" in obj &&
|
|
typeof (obj as Record<string, unknown>).inviterId === "string";
|
|
}
|
|
|
|
/**
|
|
* API
|
|
*/
|
|
|
|
export enum APIErrorCode {
|
|
BAD_REQUEST = "BAD_REQUEST",
|
|
NOT_FOUND = "NOT_FOUND",
|
|
SERVER_ERROR = "SERVER_ERROR",
|
|
TIMEOUT = "TIMEOUT",
|
|
UNAUTHORIZED = "UNAUTHORIZED",
|
|
VALIDATION_ERROR = "VALIDATION_ERROR",
|
|
}
|
|
|
|
export interface APIError {
|
|
code: APIErrorCode;
|
|
message: string;
|
|
}
|
|
|
|
export interface APISuccess<T> {
|
|
success: true;
|
|
data: T;
|
|
error?: never;
|
|
}
|
|
|
|
export interface APIFailure {
|
|
success: false;
|
|
data?: never;
|
|
error: APIError;
|
|
}
|
|
|
|
export type APIResponse<T> = APISuccess<T> | APIFailure;
|
|
|
|
export interface PaginatedData<T> {
|
|
items: T[];
|
|
total: number;
|
|
hasMore: boolean;
|
|
}
|
|
|
|
export class APIException extends Error {
|
|
readonly code: APIErrorCode;
|
|
readonly status: number;
|
|
|
|
constructor(code: APIErrorCode, status: number, message: string) {
|
|
super(message);
|
|
this.code = code;
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Comments
|
|
*/
|
|
|
|
export interface Comment {
|
|
id: string;
|
|
dumpId: string;
|
|
userId: string;
|
|
parentId?: string;
|
|
body: string;
|
|
createdAt: Date;
|
|
updatedAt?: Date;
|
|
deleted: boolean;
|
|
authorUsername: string;
|
|
authorAvatarMime?: string;
|
|
}
|
|
|
|
export interface CreateCommentRequest {
|
|
body: string;
|
|
parentId?: string;
|
|
}
|
|
|
|
export function isCreateCommentRequest(
|
|
obj: unknown,
|
|
): obj is CreateCommentRequest {
|
|
if (!obj || typeof obj !== "object") return false;
|
|
const o = obj as Record<string, unknown>;
|
|
return typeof o.body === "string" && (o.body as string).trim().length > 0 &&
|
|
(!("parentId" in o) || typeof o.parentId === "string" ||
|
|
o.parentId === null);
|
|
}
|
|
|
|
export interface UpdateCommentRequest {
|
|
body: string;
|
|
}
|
|
|
|
export function isUpdateCommentRequest(
|
|
obj: unknown,
|
|
): obj is UpdateCommentRequest {
|
|
if (!obj || typeof obj !== "object") return false;
|
|
const o = obj as Record<string, unknown>;
|
|
return typeof o.body === "string" && (o.body as string).trim().length > 0;
|
|
}
|
|
|
|
/**
|
|
* Playlists
|
|
*/
|
|
|
|
export interface Playlist {
|
|
id: string;
|
|
userId: string;
|
|
title: string;
|
|
slug?: string;
|
|
description?: string;
|
|
isPublic: boolean;
|
|
createdAt: Date;
|
|
updatedAt?: Date;
|
|
imageMime?: string;
|
|
dumpCount?: number;
|
|
ownerUsername?: string;
|
|
}
|
|
|
|
export interface PlaylistWithDumps extends Playlist {
|
|
dumps: Dump[];
|
|
}
|
|
|
|
export interface PlaylistMembership {
|
|
playlist: Playlist;
|
|
hasDump: boolean;
|
|
}
|
|
|
|
export interface CreatePlaylistRequest {
|
|
title: string;
|
|
description?: string;
|
|
isPublic: boolean;
|
|
}
|
|
|
|
export interface UpdatePlaylistRequest {
|
|
title?: string;
|
|
description?: string;
|
|
isPublic?: boolean;
|
|
}
|
|
|
|
export interface ReorderPlaylistRequest {
|
|
dumpIds: string[];
|
|
}
|
|
|
|
export function isCreatePlaylistRequest(
|
|
obj: unknown,
|
|
): obj is CreatePlaylistRequest {
|
|
return !!obj && typeof obj === "object" &&
|
|
"title" in obj && typeof obj.title === "string" &&
|
|
(!("description" in obj) || typeof obj.description === "string" ||
|
|
obj.description === null) &&
|
|
"isPublic" in obj && typeof obj.isPublic === "boolean";
|
|
}
|
|
|
|
export function isUpdatePlaylistRequest(
|
|
obj: unknown,
|
|
): obj is UpdatePlaylistRequest {
|
|
return !!obj && typeof obj === "object" &&
|
|
(!("title" in obj) || typeof obj.title === "string") &&
|
|
(!("description" in obj) || typeof obj.description === "string" ||
|
|
obj.description === null) &&
|
|
(!("isPublic" in obj) || typeof obj.isPublic === "boolean");
|
|
}
|
|
|
|
export function isReorderPlaylistRequest(
|
|
obj: unknown,
|
|
): obj is ReorderPlaylistRequest {
|
|
return !!obj && typeof obj === "object" &&
|
|
"dumpIds" in obj && Array.isArray(obj.dumpIds) &&
|
|
(obj.dumpIds as unknown[]).every((id) => typeof id === "string");
|
|
}
|
|
|
|
/**
|
|
* Request DTOs
|
|
*/
|
|
|
|
export interface CreateUrlDumpRequest {
|
|
url: string;
|
|
comment?: string;
|
|
isPrivate?: boolean;
|
|
}
|
|
|
|
export function isCreateUrlDumpRequest(
|
|
obj: unknown,
|
|
): obj is CreateUrlDumpRequest {
|
|
return !!obj &&
|
|
typeof obj === "object" &&
|
|
"url" in obj && typeof obj.url === "string" &&
|
|
(!("comment" in obj) ||
|
|
typeof obj.comment === "string" || obj.comment === null) &&
|
|
(!("isPrivate" in obj) || typeof obj.isPrivate === "boolean");
|
|
}
|
|
|
|
export interface UpdateDumpRequest {
|
|
url?: string;
|
|
comment?: string;
|
|
isPrivate?: boolean;
|
|
}
|
|
|
|
export function isUpdateDumpRequest(obj: unknown): obj is UpdateDumpRequest {
|
|
return !!obj &&
|
|
typeof obj === "object" &&
|
|
(!("url" in obj) || typeof obj.url === "string" || obj.url === null) &&
|
|
(!("comment" in obj) ||
|
|
typeof obj.comment === "string" || obj.comment === null) &&
|
|
(!("isPrivate" in obj) || typeof obj.isPrivate === "boolean");
|
|
}
|
|
|
|
/**
|
|
* WebSockets
|
|
*/
|
|
|
|
export interface VoteCastMessage {
|
|
type: "vote_cast";
|
|
dumpId: string;
|
|
userId: string;
|
|
}
|
|
|
|
export interface VoteAckMessageFailure {
|
|
type: "vote_ack";
|
|
dumpId: string;
|
|
success: false;
|
|
error: APIError;
|
|
}
|
|
|
|
export interface VoteAckMessageSuccess {
|
|
type: "vote_ack";
|
|
dumpId: string;
|
|
action: "cast" | "remove";
|
|
success: true;
|
|
voteCount: number;
|
|
error?: never;
|
|
}
|
|
|
|
export type VoteAckMessage = VoteAckMessageSuccess | VoteAckMessageFailure;
|
|
|
|
export interface VoteRemoveMessage {
|
|
type: "vote_remove";
|
|
dumpId: string;
|
|
}
|
|
|
|
export interface VotesUpdateMessage {
|
|
type: "votes_update";
|
|
dumpId: string;
|
|
voteCount: number;
|
|
}
|
|
|
|
export interface OnlineUser {
|
|
userId: string;
|
|
username: string;
|
|
hasAvatar: boolean;
|
|
avatarVersion?: number;
|
|
}
|
|
|
|
export interface WelcomeMessage {
|
|
type: "welcome";
|
|
users: OnlineUser[];
|
|
myVotes: string[];
|
|
}
|
|
|
|
export interface PresenceUpdateMessage {
|
|
type: "presence_update";
|
|
users: OnlineUser[];
|
|
}
|
|
|
|
export interface PingMessage {
|
|
type: "ping";
|
|
}
|
|
|
|
export interface PongMessage {
|
|
type: "pong";
|
|
}
|
|
|
|
/**
|
|
* Follows
|
|
*/
|
|
|
|
export interface FollowStatus {
|
|
followedUserIds: string[];
|
|
followedPlaylistIds: string[];
|
|
}
|
|
|
|
/**
|
|
* Notifications
|
|
*/
|
|
|
|
export type NotificationType =
|
|
| "playlist_followed"
|
|
| "user_followed"
|
|
| "user_dump_posted"
|
|
| "playlist_dump_added"
|
|
| "dump_upvoted"
|
|
| "user_mentioned";
|
|
|
|
export interface PlaylistFollowedData {
|
|
followerId: string;
|
|
followerUsername: string;
|
|
playlistId: string;
|
|
playlistTitle: string;
|
|
}
|
|
|
|
export interface UserFollowedData {
|
|
followerId: string;
|
|
followerUsername: string;
|
|
}
|
|
|
|
export interface UserDumpPostedData {
|
|
dumperId: string;
|
|
dumperUsername: string;
|
|
dumpId: string;
|
|
dumpTitle: string;
|
|
}
|
|
|
|
export interface PlaylistDumpAddedData {
|
|
dumpId: string;
|
|
dumpTitle: string;
|
|
playlistId: string;
|
|
playlistTitle: string;
|
|
}
|
|
|
|
export interface DumpUpvotedData {
|
|
voterId: string;
|
|
voterUsername: string;
|
|
dumpId: string;
|
|
dumpTitle: string;
|
|
}
|
|
|
|
export interface UserMentionedData {
|
|
mentionerId: string;
|
|
mentionerUsername: string;
|
|
contextType: "comment" | "dump" | "playlist";
|
|
contextId: string;
|
|
contextTitle: string;
|
|
dumpId?: string;
|
|
}
|
|
|
|
export type NotificationData =
|
|
| PlaylistFollowedData
|
|
| UserFollowedData
|
|
| UserDumpPostedData
|
|
| PlaylistDumpAddedData
|
|
| DumpUpvotedData
|
|
| UserMentionedData;
|
|
|
|
export interface Notification {
|
|
id: string;
|
|
userId: string;
|
|
type: NotificationType;
|
|
data: NotificationData;
|
|
read: boolean;
|
|
createdAt: Date;
|
|
}
|