feat(sms): add real instance message workflow

This commit is contained in:
chick
2026-07-19 02:12:57 +08:00
parent 38a26c2ce1
commit 0112e3320a
17 changed files with 781 additions and 450 deletions
@@ -20,6 +20,10 @@ import {
DeleteInstanceOperationError,
} from '../../application/operations/delete-instance-operation.js';
import type { InstanceResourceService } from '../../application/resources/instance-resource-service.js';
import {
MessageServiceError,
type InstanceMessageService,
} from '../../application/messages/instance-message-service.js';
export interface InstanceRoutesOptions {
readonly instances: InstanceService;
@@ -27,6 +31,7 @@ export interface InstanceRoutesOptions {
readonly login?: InstanceLoginService;
readonly deletion?: DeleteInstanceOperation;
readonly resources?: InstanceResourceService;
readonly messages?: InstanceMessageService;
readonly registerDeletionPreparationRoute?: boolean;
}
const problem = (
@@ -303,6 +308,21 @@ export function registerInstanceRoutes(app: FastifyInstance, options: InstanceRo
'The requested session operation could not be completed.',
),
);
if (error instanceof MessageServiceError) {
const status =
error.code === 'NOT_FOUND' ? 404 : error.code === 'VALIDATION_FAILED' ? 400 : 502;
return reply
.code(status)
.type('application/problem+json')
.send(
problem(
request,
status,
error.code,
'The requested message operation could not be completed.',
),
);
}
throw error;
}
};
@@ -345,6 +365,55 @@ export function registerInstanceRoutes(app: FastifyInstance, options: InstanceRo
options.resources!.get((request.params as { instanceId: string }).instanceId),
),
);
if (options.messages) {
app.get(
'/api/v1/instances/:instanceId/messages',
wrap(async (request) => {
const query = request.query as Record<string, unknown>;
if (Object.keys(query).some((key) => !['limit', 'offset', 'direction'].includes(key)))
throw new MessageServiceError('VALIDATION_FAILED');
const integer = (key: 'limit' | 'offset', fallback: number): number => {
const raw = query[key];
if (raw === undefined) return fallback;
if (typeof raw !== 'string' || !/^\d+$/u.test(raw))
throw new MessageServiceError('VALIDATION_FAILED');
return Number(raw);
};
const direction = query.direction;
if (direction !== undefined && direction !== 'incoming' && direction !== 'outgoing')
throw new MessageServiceError('VALIDATION_FAILED');
return options.messages!.list((request.params as { instanceId: string }).instanceId, {
limit: integer('limit', 50),
offset: integer('offset', 0),
...(direction ? { direction } : {}),
});
}),
);
app.post(
'/api/v1/instances/:instanceId/messages/send',
{
bodyLimit: 8_192,
schema: {
body: {
type: 'object',
additionalProperties: false,
required: ['phoneNumber', 'content'],
properties: {
phoneNumber: { type: 'string', minLength: 3, maxLength: 32 },
content: { type: 'string', minLength: 1, maxLength: 1600 },
},
},
},
},
wrap(async (request) => {
const value = record(request.body);
return options.messages!.send((request.params as { instanceId: string }).instanceId, {
phoneNumber: typeof value.phoneNumber === 'string' ? value.phoneNumber : '',
content: typeof value.content === 'string' ? value.content : '',
});
}),
);
}
app.patch(
'/api/v1/instances/:instanceId',
{ schema: { body: instancePatchSchema } },