add: add feature to schedule messages
This commit is contained in:
@@ -133,6 +133,49 @@ class ScheduledMessagesController {
|
||||
res.status(500).json({ success: false, message: 'Nachricht konnte nicht gesendet werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getOneTimeMessages(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const messages = await scheduledMessagesService.getOneTimeMessages();
|
||||
res.json({ success: true, data: messages });
|
||||
} catch (error) {
|
||||
logger.error('ScheduledMessagesController.getOneTimeMessages error', { error });
|
||||
res.status(500).json({ success: false, message: 'Einzelnachrichten konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async createOneTimeMessage(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { message, target_room_token, target_room_name, send_at } = req.body as Record<string, string>;
|
||||
if (!message || !target_room_token || !send_at) {
|
||||
res.status(400).json({ success: false, message: 'message, target_room_token und send_at sind erforderlich' });
|
||||
return;
|
||||
}
|
||||
const msg = await scheduledMessagesService.createOneTimeMessage(
|
||||
{ message, target_room_token, target_room_name: target_room_name ?? null, send_at },
|
||||
req.user!.id,
|
||||
);
|
||||
res.status(201).json({ success: true, data: msg });
|
||||
} catch (error) {
|
||||
logger.error('ScheduledMessagesController.createOneTimeMessage error', { error });
|
||||
res.status(500).json({ success: false, message: 'Einzelnachricht konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteOneTimeMessage(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params as Record<string, string>;
|
||||
const deleted = await scheduledMessagesService.deleteOneTimeMessage(id);
|
||||
if (!deleted) {
|
||||
res.status(404).json({ success: false, message: 'Einzelnachricht nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
logger.error('ScheduledMessagesController.deleteOneTimeMessage error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Einzelnachricht konnte nicht gelöscht werden' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ScheduledMessagesController();
|
||||
|
||||
16
backend/src/database/migrations/096_one_time_messages.sql
Normal file
16
backend/src/database/migrations/096_one_time_messages.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Migration 096: One-time scheduled messages
|
||||
-- Lightweight table for single-delivery messages at a specific datetime.
|
||||
-- Rows are deleted automatically by the job after successful send.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scheduled_one_time_messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
message TEXT NOT NULL,
|
||||
target_room_token TEXT NOT NULL,
|
||||
target_room_name TEXT,
|
||||
send_at TIMESTAMPTZ NOT NULL,
|
||||
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sotm_send_at
|
||||
ON scheduled_one_time_messages(send_at);
|
||||
@@ -81,6 +81,9 @@ async function runScheduledMessagesCheck(): Promise<void> {
|
||||
if (processed > 0) {
|
||||
logger.info(`ScheduledMessagesJob: processed ${processed} rules`);
|
||||
}
|
||||
|
||||
// Check one-time messages
|
||||
await scheduledMessagesService.sendDueOneTimeMessages();
|
||||
} catch (error) {
|
||||
logger.error('ScheduledMessagesJob: unexpected error', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { requirePermission } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// /rooms MUST come before /:id to avoid being captured as an id param
|
||||
// /rooms and /one-time MUST come before /:id to avoid being captured as an id param
|
||||
router.get(
|
||||
'/rooms',
|
||||
authenticate,
|
||||
@@ -13,6 +13,27 @@ router.get(
|
||||
scheduledMessagesController.getRooms.bind(scheduledMessagesController),
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/one-time',
|
||||
authenticate,
|
||||
requirePermission('scheduled_messages:edit'),
|
||||
scheduledMessagesController.getOneTimeMessages.bind(scheduledMessagesController),
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/one-time',
|
||||
authenticate,
|
||||
requirePermission('scheduled_messages:edit'),
|
||||
scheduledMessagesController.createOneTimeMessage.bind(scheduledMessagesController),
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/one-time/:id',
|
||||
authenticate,
|
||||
requirePermission('scheduled_messages:edit'),
|
||||
scheduledMessagesController.deleteOneTimeMessage.bind(scheduledMessagesController),
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
|
||||
@@ -521,6 +521,69 @@ async function sendVehicleEvent(vehicleId: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── One-Time Messages ─────────────────────────────────────────────────────────
|
||||
|
||||
interface OneTimeMessage {
|
||||
id: string;
|
||||
message: string;
|
||||
target_room_token: string;
|
||||
target_room_name: string | null;
|
||||
send_at: string;
|
||||
created_by: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
async function getOneTimeMessages(): Promise<OneTimeMessage[]> {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM scheduled_one_time_messages ORDER BY send_at ASC',
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async function createOneTimeMessage(
|
||||
data: Pick<OneTimeMessage, 'message' | 'target_room_token' | 'target_room_name' | 'send_at'>,
|
||||
userId: string,
|
||||
): Promise<OneTimeMessage> {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO scheduled_one_time_messages
|
||||
(message, target_room_token, target_room_name, send_at, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *`,
|
||||
[data.message, data.target_room_token, data.target_room_name ?? null, data.send_at, userId],
|
||||
);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
async function deleteOneTimeMessage(id: string): Promise<boolean> {
|
||||
const result = await pool.query(
|
||||
'DELETE FROM scheduled_one_time_messages WHERE id = $1',
|
||||
[id],
|
||||
);
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
async function sendDueOneTimeMessages(): Promise<void> {
|
||||
const creds = await getBotCredentials();
|
||||
if (!creds) return;
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM scheduled_one_time_messages WHERE send_at <= NOW() ORDER BY send_at ASC`,
|
||||
);
|
||||
|
||||
for (const row of result.rows as OneTimeMessage[]) {
|
||||
try {
|
||||
await nextcloudService.sendMessage(row.target_room_token, row.message, creds.username, creds.appPassword);
|
||||
await pool.query('DELETE FROM scheduled_one_time_messages WHERE id = $1', [row.id]);
|
||||
logger.info('scheduledMessages: sent one-time message', { id: row.id, room: row.target_room_token });
|
||||
} catch (error) {
|
||||
logger.error('scheduledMessages: failed to send one-time message', {
|
||||
id: row.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Export ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const scheduledMessagesService = {
|
||||
@@ -535,7 +598,11 @@ const scheduledMessagesService = {
|
||||
getSubscriptionsForUser,
|
||||
buildAndSend,
|
||||
sendVehicleEvent,
|
||||
getOneTimeMessages,
|
||||
createOneTimeMessage,
|
||||
deleteOneTimeMessage,
|
||||
sendDueOneTimeMessages,
|
||||
};
|
||||
|
||||
export type { ScheduledMessageRule, RoomInfo, RoomsResult };
|
||||
export type { ScheduledMessageRule, RoomInfo, RoomsResult, OneTimeMessage };
|
||||
export default scheduledMessagesService;
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
ScheduledMessagesListResponse,
|
||||
ScheduledMessageDetailResponse,
|
||||
RoomsResponse,
|
||||
OneTimeMessage,
|
||||
} from '../types/scheduledMessages.types';
|
||||
|
||||
export const scheduledMessagesApi = {
|
||||
@@ -33,4 +34,18 @@ export const scheduledMessagesApi = {
|
||||
|
||||
trigger: (id: string) =>
|
||||
api.post(`/scheduled-messages/${id}/trigger`).then(r => r.data),
|
||||
|
||||
getOneTimeMessages: () =>
|
||||
api.get<{ data: OneTimeMessage[] }>('/scheduled-messages/one-time').then(r => r.data),
|
||||
|
||||
createOneTimeMessage: (data: {
|
||||
message: string;
|
||||
target_room_token: string;
|
||||
target_room_name?: string;
|
||||
send_at: string;
|
||||
}) =>
|
||||
api.post<{ data: OneTimeMessage }>('/scheduled-messages/one-time', data).then(r => r.data),
|
||||
|
||||
deleteOneTimeMessage: (id: string) =>
|
||||
api.delete(`/scheduled-messages/one-time/${id}`).then(r => r.data),
|
||||
};
|
||||
|
||||
@@ -51,3 +51,13 @@ export interface RoomsResponse {
|
||||
data?: NextcloudRoom[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface OneTimeMessage {
|
||||
id: string;
|
||||
message: string;
|
||||
target_room_token: string;
|
||||
target_room_name: string | null;
|
||||
send_at: string;
|
||||
created_by: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user