- MessageBubble: role hidden by default, appears on hover of sender name (group/name CSS) - Direct chat header: shows other user's role (from otherUserRole field added to backend) - @mention: typing @ triggers filtered user dropdown, click inserts @Name into message - Hint text updated: '@ — упомянуть' - Backend: other_user_role added to rooms query Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
486 lines
22 KiB
TypeScript
486 lines
22 KiB
TypeScript
import { FastifyPluginAsync } from 'fastify'
|
|
import { db } from '../db'
|
|
|
|
type SlugParam = { Params: { slug: string } }
|
|
type RoomParam = { Params: { slug: string; roomId: string } }
|
|
type MsgParam = { Params: { slug: string; roomId: string; msgId: string } }
|
|
|
|
const chatRoutes: FastifyPluginAsync = async (fastify) => {
|
|
const getHotelId = async (slug: string) => {
|
|
const { rows } = await db.query('SELECT id FROM hotels WHERE slug = $1', [slug])
|
|
return rows[0]?.id as string | undefined
|
|
}
|
|
|
|
const canAccess = (userSlug: string | null, role: string, slug: string) =>
|
|
role === 'super_admin' || userSlug === slug
|
|
|
|
const ensureRoom = async (hotelId: string, type: string, name: string) => {
|
|
const { rows: existing } = await db.query(
|
|
`SELECT id FROM chat_rooms WHERE hotel_id = $1 AND type = $2 LIMIT 1`,
|
|
[hotelId, type],
|
|
)
|
|
if (existing[0]) return existing[0].id as string
|
|
const { rows } = await db.query(
|
|
`INSERT INTO chat_rooms (hotel_id, type, name) VALUES ($1, $2, $3)
|
|
ON CONFLICT DO NOTHING RETURNING id`,
|
|
[hotelId, type, name],
|
|
)
|
|
if (rows[0]) return rows[0].id as string
|
|
const { rows: r2 } = await db.query(
|
|
`SELECT id FROM chat_rooms WHERE hotel_id = $1 AND type = $2 LIMIT 1`,
|
|
[hotelId, type],
|
|
)
|
|
return r2[0]?.id as string
|
|
}
|
|
|
|
const ensureGeneralRoom = (hotelId: string) => ensureRoom(hotelId, 'general', 'Общий чат')
|
|
const ensureNotificationsRoom = (hotelId: string) => ensureRoom(hotelId, 'notifications', 'Уведомления')
|
|
|
|
// Helper: fetch full message with reactions
|
|
const fetchMessage = async (msgId: string, userId: string) => {
|
|
const { rows } = await db.query(
|
|
`SELECT m.id, m.room_id, m.sender_id, m.text, m.created_at,
|
|
m.is_system, m.system_name, m.attachment_url, m.edited_at, m.deleted_at,
|
|
COALESCE(m.system_name, u.name) AS sender_name,
|
|
COALESCE(u.role, 'system') AS sender_role,
|
|
COALESCE(
|
|
(SELECT json_agg(r ORDER BY r.first_at)
|
|
FROM (SELECT emoji,
|
|
COUNT(*)::int AS count,
|
|
bool_or(user_id = $2) AS has_own,
|
|
MIN(created_at) AS first_at
|
|
FROM chat_reactions WHERE message_id = m.id
|
|
GROUP BY emoji) r
|
|
),
|
|
'[]'::json
|
|
) AS reactions
|
|
FROM chat_messages m
|
|
LEFT JOIN users u ON u.id = m.sender_id
|
|
WHERE m.id = $1`,
|
|
[msgId, userId],
|
|
)
|
|
return rows[0]
|
|
}
|
|
|
|
// ── GET rooms ─────────────────────────────────────────────────────────────
|
|
|
|
fastify.get<SlugParam>(
|
|
'/api/hotels/:slug/chat/rooms',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { slug } = request.params
|
|
if (!canAccess(request.user.hotelSlug, request.user.role, slug))
|
|
return reply.code(403).send({ error: 'Forbidden' })
|
|
const hotelId = await getHotelId(slug)
|
|
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
|
|
|
await ensureGeneralRoom(hotelId)
|
|
await ensureNotificationsRoom(hotelId)
|
|
const userId = request.user.sub
|
|
|
|
const { rows } = await db.query(
|
|
`SELECT r.id, r.type, r.name,
|
|
(SELECT COUNT(*) FROM chat_messages m
|
|
WHERE m.room_id = r.id AND m.deleted_at IS NULL
|
|
AND m.created_at > COALESCE(
|
|
(SELECT rs.last_read FROM chat_read_status rs WHERE rs.room_id = r.id AND rs.user_id = $2),
|
|
'1970-01-01'
|
|
)
|
|
AND (m.sender_id != $2 OR m.sender_id IS NULL)
|
|
) AS unread_count,
|
|
(SELECT CASE WHEN m.deleted_at IS NOT NULL THEN 'Сообщение удалено'
|
|
WHEN m.attachment_url IS NOT NULL AND m.text = '' THEN '📷 Фото'
|
|
ELSE m.text END
|
|
FROM chat_messages m WHERE m.room_id = r.id ORDER BY m.created_at DESC LIMIT 1) AS last_message,
|
|
(SELECT m.created_at FROM chat_messages m WHERE m.room_id = r.id ORDER BY m.created_at DESC LIMIT 1) AS last_message_at,
|
|
(SELECT COALESCE(m.system_name, u.name) FROM chat_messages m LEFT JOIN users u ON u.id = m.sender_id WHERE m.room_id = r.id ORDER BY m.created_at DESC LIMIT 1) AS last_sender,
|
|
(SELECT u.name FROM chat_room_members crm JOIN users u ON u.id = crm.user_id WHERE crm.room_id = r.id AND crm.user_id != $2 LIMIT 1) AS other_user_name,
|
|
(SELECT u.role FROM chat_room_members crm JOIN users u ON u.id = crm.user_id WHERE crm.room_id = r.id AND crm.user_id != $2 LIMIT 1) AS other_user_role,
|
|
(SELECT crm.user_id FROM chat_room_members crm WHERE crm.room_id = r.id AND crm.user_id != $2 LIMIT 1) AS other_user_id,
|
|
(SELECT rs.last_read FROM chat_read_status rs WHERE rs.room_id = r.id AND rs.user_id != $2 LIMIT 1) AS other_user_last_read
|
|
FROM chat_rooms r
|
|
WHERE r.hotel_id = $1
|
|
AND (r.type IN ('general', 'notifications') OR EXISTS (
|
|
SELECT 1 FROM chat_room_members crm WHERE crm.room_id = r.id AND crm.user_id = $2
|
|
))
|
|
ORDER BY last_message_at DESC NULLS LAST, r.type = 'general' DESC`,
|
|
[hotelId, userId],
|
|
)
|
|
return rows
|
|
},
|
|
)
|
|
|
|
// ── GET messages ──────────────────────────────────────────────────────────
|
|
|
|
fastify.get<RoomParam & { Querystring: { limit?: string; before?: string } }>(
|
|
'/api/hotels/:slug/chat/rooms/:roomId/messages',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { slug, roomId } = request.params
|
|
if (!canAccess(request.user.hotelSlug, request.user.role, slug))
|
|
return reply.code(403).send({ error: 'Forbidden' })
|
|
const hotelId = await getHotelId(slug)
|
|
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
|
|
|
const limit = Math.min(Number(request.query.limit ?? 50), 100)
|
|
const before = request.query.before // ISO timestamp cursor
|
|
const userId = request.user.sub
|
|
|
|
const params: unknown[] = [roomId, hotelId, userId, limit]
|
|
const beforeClause = before ? `AND m.created_at < $5` : ''
|
|
if (before) params.push(before)
|
|
|
|
const { rows } = await db.query(
|
|
`SELECT m.id, m.room_id, m.sender_id, m.text, m.created_at,
|
|
m.is_system, m.system_name, m.attachment_url, m.edited_at, m.deleted_at,
|
|
COALESCE(m.system_name, u.name) AS sender_name,
|
|
COALESCE(u.role, 'system') AS sender_role,
|
|
COALESCE(
|
|
(SELECT json_agg(r ORDER BY r.first_at)
|
|
FROM (SELECT emoji,
|
|
COUNT(*)::int AS count,
|
|
bool_or(user_id = $3) AS has_own,
|
|
MIN(created_at) AS first_at
|
|
FROM chat_reactions WHERE message_id = m.id
|
|
GROUP BY emoji) r
|
|
),
|
|
'[]'::json
|
|
) AS reactions
|
|
FROM chat_messages m
|
|
LEFT JOIN users u ON u.id = m.sender_id
|
|
WHERE m.room_id = $1 AND m.hotel_id = $2 ${beforeClause}
|
|
ORDER BY m.created_at DESC
|
|
LIMIT $4`,
|
|
params,
|
|
)
|
|
|
|
// Only mark read on initial load (no before cursor)
|
|
if (!before) {
|
|
await db.query(
|
|
`INSERT INTO chat_read_status (room_id, user_id, last_read) VALUES ($1, $2, NOW())
|
|
ON CONFLICT (room_id, user_id) DO UPDATE SET last_read = NOW()`,
|
|
[roomId, userId],
|
|
)
|
|
}
|
|
return rows.reverse()
|
|
},
|
|
)
|
|
|
|
// ── POST message ──────────────────────────────────────────────────────────
|
|
|
|
fastify.post<RoomParam & { Body: { text?: string; attachment_url?: string } }>(
|
|
'/api/hotels/:slug/chat/rooms/:roomId/messages',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { slug, roomId } = request.params
|
|
if (!canAccess(request.user.hotelSlug, request.user.role, slug))
|
|
return reply.code(403).send({ error: 'Forbidden' })
|
|
const hotelId = await getHotelId(slug)
|
|
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
|
|
|
const { rows: roomRows } = await db.query('SELECT type FROM chat_rooms WHERE id = $1', [roomId])
|
|
if (roomRows[0]?.type === 'notifications')
|
|
return reply.code(403).send({ error: 'Cannot post to notifications room' })
|
|
|
|
const { text, attachment_url } = request.body
|
|
if (!text?.trim() && !attachment_url) return reply.code(400).send({ error: 'Text or attachment required' })
|
|
|
|
const { rows } = await db.query(
|
|
`INSERT INTO chat_messages (room_id, hotel_id, sender_id, text, attachment_url)
|
|
VALUES ($1, $2, $3, $4, $5) RETURNING id`,
|
|
[roomId, hotelId, request.user.sub, text?.trim() ?? '', attachment_url ?? null],
|
|
)
|
|
const msg = await fetchMessage(rows[0].id, request.user.sub)
|
|
return reply.code(201).send(msg)
|
|
},
|
|
)
|
|
|
|
// ── PATCH message (edit) ──────────────────────────────────────────────────
|
|
|
|
fastify.patch<MsgParam & { Body: { text: string } }>(
|
|
'/api/hotels/:slug/chat/rooms/:roomId/messages/:msgId',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { slug, msgId } = request.params
|
|
if (!canAccess(request.user.hotelSlug, request.user.role, slug))
|
|
return reply.code(403).send({ error: 'Forbidden' })
|
|
|
|
const { rows } = await db.query(
|
|
`SELECT sender_id, is_system, deleted_at FROM chat_messages WHERE id = $1`, [msgId],
|
|
)
|
|
const msg = rows[0]
|
|
if (!msg) return reply.code(404).send({ error: 'Message not found' })
|
|
if (msg.is_system) return reply.code(403).send({ error: 'Cannot edit system message' })
|
|
if (msg.deleted_at) return reply.code(400).send({ error: 'Message is deleted' })
|
|
if (msg.sender_id !== request.user.sub)
|
|
return reply.code(403).send({ error: 'Can only edit own messages' })
|
|
|
|
const { text } = request.body
|
|
if (!text?.trim()) return reply.code(400).send({ error: 'Text required' })
|
|
|
|
await db.query(
|
|
`UPDATE chat_messages SET text = $1, edited_at = NOW() WHERE id = $2`,
|
|
[text.trim(), msgId],
|
|
)
|
|
const updated = await fetchMessage(msgId, request.user.sub)
|
|
return updated
|
|
},
|
|
)
|
|
|
|
// ── DELETE message (soft) ─────────────────────────────────────────────────
|
|
|
|
fastify.delete<MsgParam>(
|
|
'/api/hotels/:slug/chat/rooms/:roomId/messages/:msgId',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { slug, msgId } = request.params
|
|
if (!canAccess(request.user.hotelSlug, request.user.role, slug))
|
|
return reply.code(403).send({ error: 'Forbidden' })
|
|
|
|
const { rows } = await db.query(
|
|
`SELECT sender_id, is_system FROM chat_messages WHERE id = $1`, [msgId],
|
|
)
|
|
const msg = rows[0]
|
|
if (!msg) return reply.code(404).send({ error: 'Message not found' })
|
|
if (msg.is_system) return reply.code(403).send({ error: 'Cannot delete system message' })
|
|
|
|
const isAdmin = ['super_admin', 'hotel_admin', 'manager'].includes(request.user.role)
|
|
if (msg.sender_id !== request.user.sub && !isAdmin)
|
|
return reply.code(403).send({ error: 'Can only delete own messages' })
|
|
|
|
await db.query(
|
|
`UPDATE chat_messages SET deleted_at = NOW(), text = '', attachment_url = NULL WHERE id = $1`,
|
|
[msgId],
|
|
)
|
|
return { ok: true, id: msgId }
|
|
},
|
|
)
|
|
|
|
// ── POST reaction (toggle) ────────────────────────────────────────────────
|
|
|
|
fastify.post<MsgParam & { Body: { emoji: string } }>(
|
|
'/api/hotels/:slug/chat/rooms/:roomId/messages/:msgId/reactions',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { slug, msgId } = request.params
|
|
if (!canAccess(request.user.hotelSlug, request.user.role, slug))
|
|
return reply.code(403).send({ error: 'Forbidden' })
|
|
|
|
const { emoji } = request.body
|
|
if (!emoji) return reply.code(400).send({ error: 'Emoji required' })
|
|
|
|
const userId = request.user.sub
|
|
// Toggle: if exists — remove, else — add
|
|
const { rows } = await db.query(
|
|
`SELECT 1 FROM chat_reactions WHERE message_id = $1 AND user_id = $2 AND emoji = $3`,
|
|
[msgId, userId, emoji],
|
|
)
|
|
if (rows[0]) {
|
|
await db.query(
|
|
`DELETE FROM chat_reactions WHERE message_id = $1 AND user_id = $2 AND emoji = $3`,
|
|
[msgId, userId, emoji],
|
|
)
|
|
} else {
|
|
await db.query(
|
|
`INSERT INTO chat_reactions (message_id, user_id, emoji) VALUES ($1, $2, $3)`,
|
|
[msgId, userId, emoji],
|
|
)
|
|
}
|
|
const updated = await fetchMessage(msgId, userId)
|
|
return updated
|
|
},
|
|
)
|
|
|
|
// ── POST direct room ──────────────────────────────────────────────────────
|
|
|
|
fastify.post<{ Params: { slug: string; otherUserId: string } }>(
|
|
'/api/hotels/:slug/chat/direct/:otherUserId',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { slug, otherUserId } = request.params
|
|
if (!canAccess(request.user.hotelSlug, request.user.role, slug))
|
|
return reply.code(403).send({ error: 'Forbidden' })
|
|
const hotelId = await getHotelId(slug)
|
|
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
|
|
|
const userId = request.user.sub
|
|
const { rows: existing } = await db.query(
|
|
`SELECT r.id FROM chat_rooms r
|
|
JOIN chat_room_members m1 ON m1.room_id = r.id AND m1.user_id = $1
|
|
JOIN chat_room_members m2 ON m2.room_id = r.id AND m2.user_id = $2
|
|
WHERE r.hotel_id = $3 AND r.type = 'direct' LIMIT 1`,
|
|
[userId, otherUserId, hotelId],
|
|
)
|
|
if (existing[0]) return { room_id: existing[0].id }
|
|
|
|
const { rows: [room] } = await db.query(
|
|
`INSERT INTO chat_rooms (hotel_id, type) VALUES ($1, 'direct') RETURNING id`, [hotelId],
|
|
)
|
|
await db.query(
|
|
`INSERT INTO chat_room_members (room_id, user_id) VALUES ($1, $2), ($1, $3)`,
|
|
[room.id, userId, otherUserId],
|
|
)
|
|
return reply.code(201).send({ room_id: room.id })
|
|
},
|
|
)
|
|
|
|
// ── PATCH mark read ───────────────────────────────────────────────────────
|
|
|
|
fastify.patch<RoomParam>(
|
|
'/api/hotels/:slug/chat/rooms/:roomId/read',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { roomId } = request.params
|
|
await db.query(
|
|
`INSERT INTO chat_read_status (room_id, user_id, last_read) VALUES ($1, $2, NOW())
|
|
ON CONFLICT (room_id, user_id) DO UPDATE SET last_read = NOW()`,
|
|
[roomId, request.user.sub],
|
|
)
|
|
return { ok: true }
|
|
},
|
|
)
|
|
|
|
// ── GET search ────────────────────────────────────────────────────────────
|
|
|
|
fastify.get<SlugParam & { Querystring: { q?: string } }>(
|
|
'/api/hotels/:slug/chat/search',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { slug } = request.params
|
|
if (!canAccess(request.user.hotelSlug, request.user.role, slug))
|
|
return reply.code(403).send({ error: 'Forbidden' })
|
|
const hotelId = await getHotelId(slug)
|
|
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
|
|
|
const q = (request.query.q ?? '').trim()
|
|
if (!q) return []
|
|
const userId = request.user.sub
|
|
|
|
const { rows } = await db.query(
|
|
`SELECT m.id, m.room_id, m.text, m.created_at,
|
|
COALESCE(m.system_name, u.name) AS sender_name,
|
|
r.type AS room_type, r.name AS room_name,
|
|
(SELECT u2.name FROM chat_room_members crm2 JOIN users u2 ON u2.id = crm2.user_id
|
|
WHERE crm2.room_id = r.id AND crm2.user_id != $2 LIMIT 1) AS other_user_name
|
|
FROM chat_messages m
|
|
JOIN chat_rooms r ON r.id = m.room_id
|
|
LEFT JOIN users u ON u.id = m.sender_id
|
|
WHERE m.hotel_id = $1 AND m.deleted_at IS NULL AND m.text ILIKE $3
|
|
AND (r.type IN ('general', 'notifications') OR EXISTS (
|
|
SELECT 1 FROM chat_room_members crm WHERE crm.room_id = r.id AND crm.user_id = $2
|
|
))
|
|
ORDER BY m.created_at DESC LIMIT 30`,
|
|
[hotelId, userId, `%${q}%`],
|
|
)
|
|
return rows
|
|
},
|
|
)
|
|
|
|
// ── POST presence (heartbeat) ─────────────────────────────────────────────
|
|
|
|
fastify.post<SlugParam>(
|
|
'/api/hotels/:slug/chat/presence',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { slug } = request.params
|
|
if (!canAccess(request.user.hotelSlug, request.user.role, slug)) return reply.code(403).send({ error: 'Forbidden' })
|
|
const hotelId = await getHotelId(slug)
|
|
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
|
await db.query(
|
|
`INSERT INTO chat_presence (user_id, hotel_id, last_seen) VALUES ($1, $2, NOW())
|
|
ON CONFLICT (user_id, hotel_id) DO UPDATE SET last_seen = NOW()`,
|
|
[request.user.sub, hotelId],
|
|
)
|
|
return { ok: true }
|
|
},
|
|
)
|
|
|
|
// ── GET online users ──────────────────────────────────────────────────────
|
|
|
|
fastify.get<SlugParam>(
|
|
'/api/hotels/:slug/chat/presence',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { slug } = request.params
|
|
if (!canAccess(request.user.hotelSlug, request.user.role, slug)) return reply.code(403).send({ error: 'Forbidden' })
|
|
const hotelId = await getHotelId(slug)
|
|
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
|
const { rows } = await db.query(
|
|
`SELECT user_id FROM chat_presence WHERE hotel_id = $1 AND last_seen > NOW() - INTERVAL '90 seconds'`,
|
|
[hotelId],
|
|
)
|
|
return rows.map((r: { user_id: string }) => r.user_id)
|
|
},
|
|
)
|
|
|
|
// ── POST typing ───────────────────────────────────────────────────────────
|
|
|
|
fastify.post<MsgParam & { Body: { typing: boolean } }>(
|
|
'/api/hotels/:slug/chat/rooms/:roomId/typing',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { roomId } = request.params
|
|
const { typing } = request.body
|
|
const { rows: uRows } = await db.query('SELECT name FROM users WHERE id = $1', [request.user.sub])
|
|
const name = uRows[0]?.name ?? 'Кто-то'
|
|
if (typing) {
|
|
await db.query(
|
|
`INSERT INTO chat_typing (user_id, room_id, name, typing_at) VALUES ($1, $2, $3, NOW())
|
|
ON CONFLICT (user_id, room_id) DO UPDATE SET typing_at = NOW(), name = $3`,
|
|
[request.user.sub, roomId, name],
|
|
)
|
|
} else {
|
|
await db.query('DELETE FROM chat_typing WHERE user_id = $1 AND room_id = $2', [request.user.sub, roomId])
|
|
}
|
|
return { ok: true }
|
|
},
|
|
)
|
|
|
|
// ── GET typing ────────────────────────────────────────────────────────────
|
|
|
|
fastify.get<RoomParam>(
|
|
'/api/hotels/:slug/chat/rooms/:roomId/typing',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { roomId } = request.params
|
|
const userId = request.user.sub
|
|
const { rows } = await db.query(
|
|
`SELECT name FROM chat_typing WHERE room_id = $1 AND user_id != $2 AND typing_at > NOW() - INTERVAL '5 seconds'`,
|
|
[roomId, userId],
|
|
)
|
|
return rows.map((r: { name: string }) => r.name)
|
|
},
|
|
)
|
|
|
|
// ── POST notify ───────────────────────────────────────────────────────────
|
|
|
|
fastify.post<SlugParam & { Body: { text: string; system_name?: string } }>(
|
|
'/api/hotels/:slug/chat/notify',
|
|
{ onRequest: [fastify.authenticate] },
|
|
async (request, reply) => {
|
|
const { slug } = request.params
|
|
if (!canAccess(request.user.hotelSlug, request.user.role, slug))
|
|
return reply.code(403).send({ error: 'Forbidden' })
|
|
if (!['super_admin', 'hotel_admin', 'manager'].includes(request.user.role))
|
|
return reply.code(403).send({ error: 'Insufficient permissions' })
|
|
|
|
const hotelId = await getHotelId(slug)
|
|
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
|
|
|
const { text, system_name = 'Система' } = request.body
|
|
if (!text?.trim()) return reply.code(400).send({ error: 'Text required' })
|
|
|
|
const notifRoomId = await ensureNotificationsRoom(hotelId)
|
|
const { rows } = await db.query(
|
|
`INSERT INTO chat_messages (room_id, hotel_id, sender_id, text, is_system, system_name)
|
|
VALUES ($1, $2, NULL, $3, true, $4) RETURNING id`,
|
|
[notifRoomId, hotelId, text.trim(), system_name],
|
|
)
|
|
const msg = await fetchMessage(rows[0].id, request.user.sub)
|
|
return reply.code(201).send(msg)
|
|
},
|
|
)
|
|
}
|
|
|
|
export default chatRoutes
|