docs: improve doucmentation
improve doucmentation and fix small store changes
This commit is contained in:
@@ -5,6 +5,15 @@ export interface OpenApiBuildOptions {
|
||||
|
||||
export function buildOpenApiDocument(options: OpenApiBuildOptions): unknown {
|
||||
const { baseUrl, appVersion } = options;
|
||||
const roomIdPathParameter = { name: 'roomId', in: 'path', required: true, schema: { type: 'string' } };
|
||||
const userIdPathParameter = { name: 'userId', in: 'path', required: true, schema: { type: 'string' } };
|
||||
const messageIdPathParameter = { name: 'messageId', in: 'path', required: true, schema: { type: 'string' } };
|
||||
const sinceTimestampQueryParameter = {
|
||||
name: 'sinceTimestamp',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'integer', minimum: 0, format: 'int64' }
|
||||
};
|
||||
|
||||
return {
|
||||
openapi: '3.1.0',
|
||||
@@ -96,6 +105,19 @@ export function buildOpenApiDocument(options: OpenApiBuildOptions): unknown {
|
||||
},
|
||||
additionalProperties: true
|
||||
},
|
||||
User: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
oderId: { type: 'string' },
|
||||
username: { type: 'string' },
|
||||
displayName: { type: 'string' },
|
||||
status: { type: 'string' },
|
||||
role: { type: 'string' },
|
||||
isOnline: { type: 'boolean' }
|
||||
},
|
||||
additionalProperties: true
|
||||
},
|
||||
Message: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -110,6 +132,59 @@ export function buildOpenApiDocument(options: OpenApiBuildOptions): unknown {
|
||||
isDeleted: { type: 'boolean' }
|
||||
},
|
||||
additionalProperties: true
|
||||
},
|
||||
Reaction: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
messageId: { type: 'string' },
|
||||
userId: { type: 'string' },
|
||||
oderId: { type: 'string' },
|
||||
emoji: { type: 'string' },
|
||||
timestamp: { type: 'integer', format: 'int64' }
|
||||
},
|
||||
additionalProperties: true
|
||||
},
|
||||
Attachment: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
messageId: { type: 'string' },
|
||||
filename: { type: 'string' },
|
||||
size: { type: 'integer' },
|
||||
mime: { type: 'string' },
|
||||
isImage: { type: 'boolean' },
|
||||
filePath: { type: 'string' },
|
||||
savedPath: { type: 'string' }
|
||||
},
|
||||
additionalProperties: true
|
||||
},
|
||||
Ban: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
oderId: { type: 'string' },
|
||||
roomId: { type: 'string' },
|
||||
userId: { type: 'string' },
|
||||
bannedBy: { type: 'string' },
|
||||
displayName: { type: 'string' },
|
||||
reason: { type: 'string' },
|
||||
expiresAt: { type: 'integer', format: 'int64' },
|
||||
timestamp: { type: 'integer', format: 'int64' }
|
||||
},
|
||||
additionalProperties: true
|
||||
},
|
||||
PluginDataValue: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
value: {}
|
||||
}
|
||||
},
|
||||
MetaValue: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string' },
|
||||
value: { type: ['string', 'null'] }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -225,11 +300,47 @@ export function buildOpenApiDocument(options: OpenApiBuildOptions): unknown {
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/rooms/{roomId}': {
|
||||
get: {
|
||||
summary: 'Get a room by id',
|
||||
parameters: [roomIdPathParameter],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Room details',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/Room' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'404': {
|
||||
description: 'Room not found',
|
||||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/rooms/{roomId}/users': {
|
||||
get: {
|
||||
summary: 'List users known for a room',
|
||||
parameters: [roomIdPathParameter],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Users array',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { type: 'array', items: { $ref: '#/components/schemas/User' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/rooms/{roomId}/messages': {
|
||||
get: {
|
||||
summary: 'List messages for a room',
|
||||
parameters: [
|
||||
{ name: 'roomId', in: 'path', required: true, schema: { type: 'string' } },
|
||||
roomIdPathParameter,
|
||||
{ name: 'limit', in: 'query', required: false, schema: { type: 'integer', minimum: 1, maximum: 500 } },
|
||||
{ name: 'offset', in: 'query', required: false, schema: { type: 'integer', minimum: 0 } }
|
||||
],
|
||||
@@ -247,6 +358,182 @@ export function buildOpenApiDocument(options: OpenApiBuildOptions): unknown {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/rooms/{roomId}/messages/since': {
|
||||
get: {
|
||||
summary: 'List room messages after a timestamp',
|
||||
parameters: [roomIdPathParameter, sinceTimestampQueryParameter],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Messages array',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { type: 'array', items: { $ref: '#/components/schemas/Message' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/rooms/{roomId}/bans': {
|
||||
get: {
|
||||
summary: 'List active bans for a room',
|
||||
parameters: [roomIdPathParameter],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Bans array',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { type: 'array', items: { $ref: '#/components/schemas/Ban' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/rooms/{roomId}/bans/{userId}': {
|
||||
get: {
|
||||
summary: 'Check whether a user is banned in a room',
|
||||
parameters: [roomIdPathParameter, userIdPathParameter],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Ban status',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['isBanned'],
|
||||
properties: { isBanned: { type: 'boolean' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/messages/{messageId}': {
|
||||
get: {
|
||||
summary: 'Get a message by id',
|
||||
parameters: [messageIdPathParameter],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Message details',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/Message' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'404': {
|
||||
description: 'Message not found',
|
||||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/messages/{messageId}/reactions': {
|
||||
get: {
|
||||
summary: 'List reactions for a message',
|
||||
parameters: [messageIdPathParameter],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Reactions array',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { type: 'array', items: { $ref: '#/components/schemas/Reaction' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/messages/{messageId}/attachments': {
|
||||
get: {
|
||||
summary: 'List attachments for a message',
|
||||
parameters: [messageIdPathParameter],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Attachments array',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { type: 'array', items: { $ref: '#/components/schemas/Attachment' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/users/{userId}': {
|
||||
get: {
|
||||
summary: 'Get a user by id',
|
||||
parameters: [userIdPathParameter],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'User details',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/User' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'404': {
|
||||
description: 'User not found',
|
||||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/attachments': {
|
||||
get: {
|
||||
summary: 'List all attachments stored on this device',
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Attachments array',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { type: 'array', items: { $ref: '#/components/schemas/Attachment' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/plugin-data': {
|
||||
get: {
|
||||
summary: 'Read a plugin data value',
|
||||
parameters: [
|
||||
{ name: 'pluginId', in: 'query', required: true, schema: { type: 'string' } },
|
||||
{ name: 'key', in: 'query', required: true, schema: { type: 'string' } },
|
||||
{ name: 'scope', in: 'query', required: true, schema: { type: 'string', enum: ['local', 'server'] } },
|
||||
{ name: 'serverId', in: 'query', required: false, schema: { type: 'string' } }
|
||||
],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Plugin data value',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/PluginDataValue' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/meta/{key}': {
|
||||
get: {
|
||||
summary: 'Read a desktop metadata value',
|
||||
parameters: [{ name: 'key', in: 'path', required: true, schema: { type: 'string' } }],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Metadata value',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/MetaValue' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,6 +104,37 @@ function clampInt(value: unknown, min: number, max: number, fallback: number): n
|
||||
return Math.max(min, Math.min(max, Math.floor(parsed)));
|
||||
}
|
||||
|
||||
function getTrailingPathParam(pathname: string, pattern: RegExp, name: string): string {
|
||||
const value = pattern.exec(pathname)?.[1];
|
||||
|
||||
if (!value) {
|
||||
throw new HttpError(400, `${name} is required`, 'INVALID_REQUEST');
|
||||
}
|
||||
|
||||
return decodeURIComponent(value);
|
||||
}
|
||||
|
||||
function getRequiredQueryParam(ctx: RouteContext, name: string): string {
|
||||
const value = ctx.request.url.searchParams.get(name)?.trim() ?? '';
|
||||
|
||||
if (!value) {
|
||||
throw new HttpError(400, `${name} is required`, 'INVALID_REQUEST');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function getRequiredTimestamp(ctx: RouteContext, name: string): number {
|
||||
const raw = getRequiredQueryParam(ctx, name);
|
||||
const value = Number(raw);
|
||||
|
||||
if (!Number.isFinite(value) || value < 0) {
|
||||
throw new HttpError(400, `${name} must be a non-negative timestamp`, 'INVALID_REQUEST');
|
||||
}
|
||||
|
||||
return Math.floor(value);
|
||||
}
|
||||
|
||||
const ROUTES: RouteDefinition[] = [
|
||||
defineRoute('GET', '/api/health', async (ctx): Promise<RouteResponse> => ({
|
||||
status: 200,
|
||||
@@ -276,20 +307,165 @@ const ROUTES: RouteDefinition[] = [
|
||||
return { status: 200, body: rooms ?? [] };
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/rooms/{roomId}', async (ctx): Promise<RouteResponse> => {
|
||||
const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)$/u, 'roomId');
|
||||
const room = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetRoom,
|
||||
payload: { roomId }
|
||||
});
|
||||
|
||||
if (!room) {
|
||||
throw new HttpError(404, 'Room not found on this device', 'ROOM_NOT_FOUND');
|
||||
}
|
||||
|
||||
return { status: 200, body: room };
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/rooms/{roomId}/users', async (ctx): Promise<RouteResponse> => {
|
||||
const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/users$/u, 'roomId');
|
||||
const users = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetUsersByRoom,
|
||||
payload: { roomId }
|
||||
});
|
||||
|
||||
return { status: 200, body: users ?? [] };
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/rooms/{roomId}/messages', async (ctx): Promise<RouteResponse> => {
|
||||
const roomId = ctx.request.url.pathname.match(/\/api\/rooms\/([^/]+)\/messages$/u)?.[1];
|
||||
|
||||
if (!roomId)
|
||||
throw new HttpError(400, 'roomId is required', 'INVALID_REQUEST');
|
||||
|
||||
const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/messages$/u, 'roomId');
|
||||
const limit = clampInt(ctx.request.url.searchParams.get('limit'), 1, 500, 100);
|
||||
const offset = clampInt(ctx.request.url.searchParams.get('offset'), 0, Number.MAX_SAFE_INTEGER, 0);
|
||||
const messages = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetMessages,
|
||||
payload: { roomId: decodeURIComponent(roomId), limit, offset }
|
||||
payload: { roomId, limit, offset }
|
||||
});
|
||||
|
||||
return { status: 200, body: messages ?? [] };
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/rooms/{roomId}/messages/since', async (ctx): Promise<RouteResponse> => {
|
||||
const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/messages\/since$/u, 'roomId');
|
||||
const sinceTimestamp = getRequiredTimestamp(ctx, 'sinceTimestamp');
|
||||
const messages = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetMessagesSince,
|
||||
payload: { roomId, sinceTimestamp }
|
||||
});
|
||||
|
||||
return { status: 200, body: messages ?? [] };
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/rooms/{roomId}/bans', async (ctx): Promise<RouteResponse> => {
|
||||
const roomId = getTrailingPathParam(ctx.request.pathname, /\/api\/rooms\/([^/]+)\/bans$/u, 'roomId');
|
||||
const bans = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetBansForRoom,
|
||||
payload: { roomId }
|
||||
});
|
||||
|
||||
return { status: 200, body: bans ?? [] };
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/rooms/{roomId}/bans/{userId}', async (ctx): Promise<RouteResponse> => {
|
||||
const match = /\/api\/rooms\/([^/]+)\/bans\/([^/]+)$/u.exec(ctx.request.pathname);
|
||||
|
||||
if (!match) {
|
||||
throw new HttpError(400, 'roomId and userId are required', 'INVALID_REQUEST');
|
||||
}
|
||||
|
||||
const isBanned = await runQuery<boolean>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.IsUserBanned,
|
||||
payload: { roomId: decodeURIComponent(match[1]), userId: decodeURIComponent(match[2]) }
|
||||
});
|
||||
|
||||
return { status: 200, body: { isBanned } };
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/messages/{messageId}', async (ctx): Promise<RouteResponse> => {
|
||||
const messageId = getTrailingPathParam(ctx.request.pathname, /\/api\/messages\/([^/]+)$/u, 'messageId');
|
||||
const message = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetMessageById,
|
||||
payload: { messageId }
|
||||
});
|
||||
|
||||
if (!message) {
|
||||
throw new HttpError(404, 'Message not found on this device', 'MESSAGE_NOT_FOUND');
|
||||
}
|
||||
|
||||
return { status: 200, body: message };
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/messages/{messageId}/reactions', async (ctx): Promise<RouteResponse> => {
|
||||
const messageId = getTrailingPathParam(ctx.request.pathname, /\/api\/messages\/([^/]+)\/reactions$/u, 'messageId');
|
||||
const reactions = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetReactionsForMessage,
|
||||
payload: { messageId }
|
||||
});
|
||||
|
||||
return { status: 200, body: reactions ?? [] };
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/messages/{messageId}/attachments', async (ctx): Promise<RouteResponse> => {
|
||||
const messageId = getTrailingPathParam(ctx.request.pathname, /\/api\/messages\/([^/]+)\/attachments$/u, 'messageId');
|
||||
const attachments = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetAttachmentsForMessage,
|
||||
payload: { messageId }
|
||||
});
|
||||
|
||||
return { status: 200, body: attachments ?? [] };
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/users/{userId}', async (ctx): Promise<RouteResponse> => {
|
||||
const userId = getTrailingPathParam(ctx.request.pathname, /\/api\/users\/([^/]+)$/u, 'userId');
|
||||
const user = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetUser,
|
||||
payload: { userId }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new HttpError(404, 'User not found on this device', 'USER_NOT_FOUND');
|
||||
}
|
||||
|
||||
return { status: 200, body: user };
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/attachments', async (ctx): Promise<RouteResponse> => {
|
||||
const attachments = await runQuery<unknown[]>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetAllAttachments,
|
||||
payload: {}
|
||||
});
|
||||
|
||||
return { status: 200, body: attachments ?? [] };
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/plugin-data', async (ctx): Promise<RouteResponse> => {
|
||||
const pluginId = getRequiredQueryParam(ctx, 'pluginId');
|
||||
const key = getRequiredQueryParam(ctx, 'key');
|
||||
const scope = getRequiredQueryParam(ctx, 'scope');
|
||||
|
||||
if (scope !== 'local' && scope !== 'server') {
|
||||
throw new HttpError(400, 'scope must be local or server', 'INVALID_REQUEST');
|
||||
}
|
||||
|
||||
const value = await runQuery<unknown>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetPluginData,
|
||||
payload: {
|
||||
key,
|
||||
pluginId,
|
||||
scope,
|
||||
serverId: ctx.request.url.searchParams.get('serverId') ?? undefined
|
||||
}
|
||||
});
|
||||
|
||||
return { status: 200, body: { value } };
|
||||
}, true),
|
||||
|
||||
defineRoute('GET', '/api/meta/{key}', async (ctx): Promise<RouteResponse> => {
|
||||
const key = getTrailingPathParam(ctx.request.pathname, /\/api\/meta\/([^/]+)$/u, 'key');
|
||||
const value = await runQuery<string | null>(requireDataSource(ctx.dataSource), {
|
||||
type: QueryType.GetMeta,
|
||||
payload: { key }
|
||||
});
|
||||
|
||||
return { status: 200, body: { key, value } };
|
||||
}, true)
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user