import express from "express"; import { body, param, validationResult } from "express-validator"; import { authenticateRoute } from "../../../auth"; import { query } from "../../../database"; import { getMessageById, getMessagesByChannelFirstPage, getMessagesByChannelPage } from "../../../database/templates"; import { errors } from "../../../errors"; import { dispatch, dispatchChannelSubscribe } from "../../../gateway"; import { GatewayPayloadType } from "../../../gateway/gatewaypayloadtype"; import serverConfig from "../../../serverconfig"; const router = express.Router(); router.post( "/", authenticateRoute(), body("name").isLength({ min: 1, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }), async (req, res) => { const validationErrors = validationResult(req); if (!validationErrors.isEmpty()) { return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); } if (serverConfig.superuserRequirement.createChannel && !req.user.is_superuser) { return res.status(403).json({ ...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS }); } const { name } = req.body; const result = await query("INSERT INTO channels(name, owner_id) VALUES ($1, $2) RETURNING id, name, owner_id", [name, req.user.id]); if (!result || result.rowCount < 1) { return res.status(500).json({ ...errors.GOT_NO_DATABASE_DATA }); } dispatch("*", { t: GatewayPayloadType.ChannelCreate, d: result.rows[0] }); // When a new channel is created, we will currently subscribe every client // on the gateway (this will be changed when the concept of "communities" is added) dispatchChannelSubscribe("*", `channel:${result.rows[0].id}`); res.status(201).send(result.rows[0]); } ); router.put( "/:id", authenticateRoute(), body("name").isLength({ min: 1, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }), param("id").isNumeric(), async (req, res) => { const validationErrors = validationResult(req); if (!validationErrors.isEmpty()) { return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); } const { name } = req.body; const id = parseInt(req.params.id); // TODO: ?? const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]); if (!permissionCheckResult || permissionCheckResult.rowCount < 1) { return res.status(404).json({ ...errors.NOT_FOUND }); } if (permissionCheckResult.rows[0].owner_id !== req.user.id && !req.user.is_superuser) { return res.status(403).json({ ...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS }); } const result = await query("UPDATE channels SET name = $1 WHERE id = $2", [name, id]); if (!result || result.rowCount < 1) { return res.status(500).json({ ...errors.GOT_NO_DATABASE_DATA }); } const updatePayload = { id, name, owner_id: permissionCheckResult.rows[0].owner_id }; dispatch(`channel:${id}`, { t: GatewayPayloadType.ChannelUpdate, d: updatePayload }); return res.status(200).send(updatePayload); } ); router.delete( "/:id", authenticateRoute(), param("id").isNumeric(), async (req, res) => { const validationErrors = validationResult(req); if (!validationErrors.isEmpty()) { return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); } const id = parseInt(req.params.id); // TODO: ?? const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]); if (!permissionCheckResult || permissionCheckResult.rowCount < 1) { return res.status(404).json({ ...errors.NOT_FOUND }); } if (permissionCheckResult.rows[0].owner_id !== req.user.id && !req.user.is_superuser) { return res.status(403).json({ ...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS }); } const result = await query("DELETE FROM channels WHERE id = $1", [id]); if (!result || result.rowCount < 1) { return res.status(500).json({ ...errors.GOT_NO_DATABASE_DATA }); } dispatch(`channel:${id}`, { t: GatewayPayloadType.ChannelDelete, d: { id } }); return res.status(204).send(""); } ); router.get( "/:id", authenticateRoute(), param("id").isNumeric(), async (req, res) => { const validationErrors = validationResult(req); if (!validationErrors.isEmpty()) { return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); } const { id } = req.params; const result = await query("SELECT id, name, owner_id FROM channels WHERE id = $1", [id]); if (!result || result.rowCount < 1) { return res.status(404).json({ ...errors.NOT_FOUND }); } return res.status(200).send(result.rows[0]); } ); router.get( "/", authenticateRoute(), async (req, res) => { const result = await query("SELECT id, name, owner_id FROM channels"); return res.status(200).send(result ? result.rows : []); } ); router.post( "/:id/messages", authenticateRoute(), param("id").isNumeric(), body("content").isLength({ min: 1, max: 4000 }), body("optimistic_id").optional().isNumeric(), async (req, res) => { const validationErrors = validationResult(req); if (!validationErrors.isEmpty()) { return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); } const optimisticId = parseInt(req.body.optimistic_id); const channelId = parseInt(req.params.id); const { content } = req.body; const authorId = req.user.id; const createdAt = Date.now().toString(); const result = await query("INSERT INTO messages(content, channel_id, author_id, created_at) VALUES ($1, $2, $3, $4) RETURNING id", [content, channelId, authorId, createdAt]); if (!result || result.rowCount < 1) { return res.status(500).json({ ...errors.GOT_NO_DATABASE_DATA }); } let returnObject: any = { id: result.rows[0].id, content, channel_id: channelId, author_id: authorId, author_username: req.user.username, created_at: createdAt }; dispatch(`channel:${channelId}`, (ws) => { let payload: any = returnObject; if (ws.state && ws.state.user && ws.state.user.id === req.user.id && optimisticId) { payload = { ...payload, optimistic_id: optimisticId } returnObject = { ...returnObject, optimistic_id: optimisticId }; } return { t: GatewayPayloadType.MessageCreate, d: payload }; }); return res.status(201).send(returnObject); } ); router.get( "/:id/messages", authenticateRoute(), param("id").isNumeric(), param("count").optional().isInt({ min: 10, max: 50 }), async (req, res) => { const validationErrors = validationResult(req); if (!validationErrors.isEmpty()) { return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); } const { before, count } = req.query; let limit = typeof count === "string" ? parseInt(count || "25") : 25; if (limit === NaN) { return res.status(400).json({ ...errors.INVALID_DATA }); } const channelId = parseInt(req.params.id); let finalRows = []; if (before) { const result = await query(getMessagesByChannelPage(limit), [before, channelId]); finalRows = result ? result.rows : []; } else { const result = await query(getMessagesByChannelFirstPage(limit), [channelId]); finalRows = result ? result.rows : []; } return res.status(200).send(finalRows); } ); router.post( "/:id/typing", authenticateRoute(), param("id").isNumeric(), async (req, res) => { const validationErrors = validationResult(req); if (!validationErrors.isEmpty()) { return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); } const channelId = parseInt(req.params.id); dispatch(`channel:${channelId}`, { t: GatewayPayloadType.TypingStart, d: { user: { id: req.publicUser.id, username: req.publicUser.username }, channel: { id: channelId }, time: 7500 } }); return res.status(201).send(""); } ); export default router;