diff --git a/concord-server/schema.prisma b/concord-server/schema.prisma index b0cc65d..bdfcf43 100644 --- a/concord-server/schema.prisma +++ b/concord-server/schema.prisma @@ -103,7 +103,7 @@ model Message { User User @relation(fields: [userId], references: [id]) userId String deleted Boolean - text String + text String @db.VarChar(2000) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt replies Reply? @relation("MessageToReply") diff --git a/concord-server/src/controller/realtime.ts b/concord-server/src/controller/realtimeController.ts similarity index 52% rename from concord-server/src/controller/realtime.ts rename to concord-server/src/controller/realtimeController.ts index 5f9b961..834e4b6 100644 --- a/concord-server/src/controller/realtime.ts +++ b/concord-server/src/controller/realtimeController.ts @@ -1,68 +1,32 @@ import { Context } from "hono"; import { - sendMessageToChannel, + sendMessageToChannelEvent, removeMessageFromChannel, } from "../services/realtime.js"; import { success } from "zod"; +import { PostMessageToChannelInput } from "../validators/realtimeValidator.js"; +import { sendMessage } from "./messageController.js"; -export async function postMessageToChannel(io: any, c: Context) { - try { - io = c.get("io"); - - const instanceId = c.req.param("instanceId"); - const categoryId = c.req.param("categoryId"); - const channelId = c.req.param("channelId"); - const message = await c.req.json(); - - const result = await sendMessageToChannel( - instanceId, - categoryId, - channelId, - message, - "new_channel_message", - io, - ); - - if (result === "Event not implemented") { - console.log( - "controller::realtime::postMessageToChannel - Failed to send message", - ); - return c.json({ - success: false, - message: "Event not implemented or recognized", - status: 400, - }); - } - - if (result === "no acknowledgment") { - console.log( - "controller::realtime::postMessageToChannel - No acknowledgment received from client", - ); - return c.json({ - success: false, - message: "No acknowledgment received from client", - status: 500, - }); - } - - if (!result) { - throw new Error("failed to send message"); - } - - return c.json({ - success: true, - message: "Message sent successfully", - status: 200, - }); - } catch (err) { - const errMessage = err as Error; - console.log("controller::realtime::postMessageToChannel - ", errMessage); - return c.json({ - success: false, - message: errMessage.message, - status: 500, - }); - } +export async function postMessageToChannel(io: any, c: Context, data: PostMessageToChannelInput) { + const instanceId = data.instanceId; + const categoryId = data.categoryId; + const channelId = data.channelId; + const userId = data.userId; + const content = data.content; + const token = data.token; + const repliedMessageId = data.repliedMessageId ?? null; + const event = "new_channel_message"; + return sendMessageToChannelEvent( + instanceId, + categoryId, + channelId, + userId, + content, + token, + repliedMessageId, + event, + io + ); } export async function deleteMessageFromChannel(io: any, c: Context) { diff --git a/concord-server/src/routes/index.ts b/concord-server/src/routes/index.ts index 84ef367..bb09610 100644 --- a/concord-server/src/routes/index.ts +++ b/concord-server/src/routes/index.ts @@ -5,6 +5,7 @@ import messageRoutes from "./messageRoutes"; import { channelRoutes } from "./channelRoutes"; import instanceRoutes from "./instanceRoutes"; import { categoryRoutes } from "./categoryRoutes"; +import realtimeRoutes from "./realtimeRoutes"; const routes = new Hono(); @@ -13,5 +14,6 @@ routes.route("/message", messageRoutes); routes.route("/channel", channelRoutes); routes.route("/instance", instanceRoutes); routes.route("/category", categoryRoutes); +routes.route("/realtime", realtimeRoutes); export default routes; diff --git a/concord-server/src/routes/realtime.ts b/concord-server/src/routes/realtime.ts deleted file mode 100644 index 933b839..0000000 --- a/concord-server/src/routes/realtime.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Hono } from "hono"; -import { zValidator } from "@hono/zod-validator"; -import { describeRoute, resolver } from "hono-openapi"; -import { - postMessageToChannel, - deleteMessageFromChannel, -} from "../controller/realtime"; - -const app = new Hono(); - -app.post( - "message/", - zValidator({ - body: z.object({ - content: z.string().min(1).max(500), - }), - }), - async (c) => { - const { instanceId, categoryId, channelId } = c.req.params; - const { content } = c.req.body; - - return postMessageToChannel(c.get("io"), { - instanceId, - categoryId, - channelId, - content, - }); - }, -); diff --git a/concord-server/src/routes/realtimeRoutes.ts b/concord-server/src/routes/realtimeRoutes.ts new file mode 100644 index 0000000..cb44dd1 --- /dev/null +++ b/concord-server/src/routes/realtimeRoutes.ts @@ -0,0 +1,90 @@ +import { Hono } from "hono"; +import { zValidator } from "@hono/zod-validator"; +import { describeRoute, resolver } from "hono-openapi"; +import { + postMessageToChannelSchema +} from "../validators/realtimeValidator.js"; +import { + postMessageToChannel, + deleteMessageFromChannel, +} from "../controller/realtimeController"; + + +const realtimeRoutes = new Hono(); + +realtimeRoutes.post( + "/message/:instanceId/:categoryId/:channelId", + describeRoute({ + description: "Post a message to a channel", + responses: { + 200: { + description: "Message posted successfully", + content: { + "application/json": { schema: resolver(postMessageToChannelSchema) }, + }, + }, + 400: { + description: "Bad Request - Invalid input data", + content: { + "application/json": { schema: resolver(postMessageToChannelSchema) }, + }, + }, + 401: { + description: "Unauthorized - Invalid token", + content: { + "application/json": { schema: resolver(postMessageToChannelSchema) }, + }, + }, + 404: { + description: "Instance, Category, Channel, or User not found", + content: { + "application/json": { schema: resolver(postMessageToChannelSchema) }, + }, + }, + 500: { + description: "Server error", + content: { + "application/json": { schema: resolver(postMessageToChannelSchema) }, + }, + }, + }, + }), + zValidator("json", postMessageToChannelSchema), + async (c) => { + const instanceId = c.req.param("instanceId"); + const categoryId = c.req.param("categoryId"); + const channelId = c.req.param("channelId"); + const { userId, content, repliedMessageId, token } = await c.req.json(); + + const ioServer = (c as any).get("io"); + if (!ioServer) { + return c.json({ success: false, error: "Realtime server not available" }, 500); + } + + const result = await postMessageToChannel(ioServer, c, { + instanceId, + categoryId, + channelId, + userId, + content, + token, + repliedMessageId: repliedMessageId ?? null, + }) + + if (result === "event not implemented") { + return c.json({ success: false, message: "Event not implemented or recognized" }, 400); + } + + if (result === "no acknowledgment") { + return c.json({ success: false, message: "No acknowledgment received from client" }, 500); + } + + if (!result) { + return c.json({ success: false, message: "Failed to post message" }, 500); + } + + return c.json({ success: true, result }, 200); + } +); + +export default realtimeRoutes; \ No newline at end of file diff --git a/concord-server/src/validators/channelValidator.ts b/concord-server/src/validators/channelValidator.ts index 23cb918..626e1fb 100644 --- a/concord-server/src/validators/channelValidator.ts +++ b/concord-server/src/validators/channelValidator.ts @@ -52,5 +52,5 @@ export type GetChannelsByCategoryIdInput = z.infer< export type UpdateChannelInput = z.infer; export type DeleteChannelInput = z.infer; export type DeleteChannelsByCategoryIdInput = z.infer< - typeof deleteChannelsByCategoryIdSchema +typeof deleteChannelsByCategoryIdSchema >; diff --git a/concord-server/src/validators/index.ts b/concord-server/src/validators/index.ts deleted file mode 100644 index 1348a03..0000000 --- a/concord-server/src/validators/index.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Hono } from "hono"; -import { cors } from "hono/cors"; -import { Server as Engine } from "@socket.io/bun-engine"; -import { Server } from "socket.io"; -import routes from "./routes/index"; -import { Scalar } from "@scalar/hono-api-reference"; -import { openAPIRouteHandler } from "hono-openapi"; - -//initialize socket.io server -const io = new Server(); - -//initialize bun engine -//then bind to socket.io server -const engine = new Engine(); -io.bind(engine); - -io.on("connection", (socket) => { - //get userId and clientId from query params - const userId = socket.handshake.query.userId; - const clientId = socket.handshake.query.clientId; - if (!userId || Array.isArray(userId)) { - socket.disconnect(); - throw new Error("Invalid user ID"); - } - - if (!clientId || Array.isArray(clientId)) { - socket.disconnect(); - throw new Error("Invalid client ID"); - } - - socket.join(userId); - console.log( - `User ${userId} connected. Client ID ${clientId} on socket ${socket.id}`, - ); - - socket.on("disconnect", () => { - console.log(`User ${userId} disconnected from socket ${socket.id}`); - }); -}); - -const app = new Hono(); - -app.use( - "*", - cors({ - origin: "http://localhost:5173", - allowHeaders: ["Content-Type", "Authorization"], - allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - credentials: true, - }), -); - -app.route("/api", routes); - -app.get( - "/openapi", - openAPIRouteHandler(app, { - documentation: { - info: { - title: "Hono API", - version: "1.0.0", - description: "Greeting API", - }, - servers: [{ url: "http://localhost:3000", description: "Local Server" }], - }, - }), -); - -app.get("/scalar", Scalar({ url: "/openapi" })); - -export default app; diff --git a/concord-server/src/validators/realtimeValidator.ts b/concord-server/src/validators/realtimeValidator.ts index e69de29..f9c971d 100644 --- a/concord-server/src/validators/realtimeValidator.ts +++ b/concord-server/src/validators/realtimeValidator.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +export const postMessageToChannelSchema = z.object({ + instanceId: z.uuidv7(), + categoryId: z.uuidv7(), + channelId: z.uuidv7(), + userId: z.uuidv7(), + content: z.string().min(1).max(2000), + repliedMessageId: z.uuidv7().optional(), + token: z.string(), +}); + +//TODO: add more realtime related validators as needed + +export type PostMessageToChannelInput = z.infer; +//TODO: create more input schemas for other realtime actions \ No newline at end of file