2025-08-28 18:27:07 +02:00
|
|
|
import { TRPCError, tracked } from "@trpc/server";
|
2025-08-07 14:50:40 +02:00
|
|
|
import { z } from "zod/v4";
|
2025-08-04 17:45:44 +02:00
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
createTRPCRouter,
|
|
|
|
|
protectedProcedure,
|
|
|
|
|
publicProcedure,
|
2025-08-04 17:45:44 +02:00
|
|
|
} from "~/server/api/trpc";
|
|
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
addTyping,
|
|
|
|
|
getCurrentlyTyping,
|
|
|
|
|
removeStaleTyping,
|
|
|
|
|
removeTyping,
|
2025-08-04 17:45:44 +02:00
|
|
|
} from "~/server/controllers/chat.controller";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { db } from "~/server/db";
|
|
|
|
|
import { add } from "~/server/services/chat.service";
|
|
|
|
|
import { ee, type OnlineStatus } from "~/server/sse";
|
|
|
|
|
import { zChatId, zUserId } from "~/server/utils/zod_types";
|
2025-08-04 17:45:44 +02:00
|
|
|
import { getKeydbClient } from "~/utils/keydb";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { getMessagesArray } from "~/utils/kysely-helper";
|
2025-08-04 17:45:44 +02:00
|
|
|
|
|
|
|
|
export const chatSSERouter = createTRPCRouter({
|
2025-08-28 18:27:07 +02:00
|
|
|
create: protectedProcedure
|
|
|
|
|
.input(z.object({ userId: zUserId }))
|
|
|
|
|
.mutation(async ({ input }) => {
|
|
|
|
|
const newChat = await add({
|
|
|
|
|
db,
|
|
|
|
|
userId: input.userId,
|
|
|
|
|
});
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return newChat.chatid;
|
|
|
|
|
}),
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
enterChat: protectedProcedure
|
|
|
|
|
.input(z.object({ chatId: zChatId }))
|
|
|
|
|
.mutation(async ({ input, ctx }) => {
|
|
|
|
|
const chatKey = `chat:${input.chatId}:onlineUsers`;
|
|
|
|
|
const keydb = getKeydbClient();
|
2025-08-04 18:59:33 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
if (!keydb) {
|
|
|
|
|
console.warn(
|
|
|
|
|
"KeyDB client is not initialized. Skipping online status.",
|
|
|
|
|
);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
// Add the user to the Redis set
|
|
|
|
|
await keydb.sadd(
|
|
|
|
|
chatKey,
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
isAdmin: ctx.session.isAdmin,
|
|
|
|
|
nome: ctx.session.nome,
|
2025-08-29 16:18:32 +02:00
|
|
|
userId: ctx.session.id,
|
2025-08-28 18:27:07 +02:00
|
|
|
}),
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const onlineUsers = await keydb.smembers(chatKey);
|
|
|
|
|
const parsedUsers = onlineUsers.map(
|
|
|
|
|
(user) => JSON.parse(user) as OnlineStatus,
|
|
|
|
|
);
|
|
|
|
|
ee.emit("chatUpdates", input.chatId, {
|
|
|
|
|
data: parsedUsers,
|
2025-08-29 16:18:32 +02:00
|
|
|
eventType: "online",
|
2025-08-28 18:27:07 +02:00
|
|
|
});
|
|
|
|
|
return parsedUsers;
|
|
|
|
|
}),
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
leaveChat: protectedProcedure
|
|
|
|
|
.input(z.object({ chatId: zChatId }))
|
|
|
|
|
.mutation(async ({ input, ctx }) => {
|
|
|
|
|
const chatKey = `chat:${input.chatId}:onlineUsers`;
|
|
|
|
|
const keydb = getKeydbClient();
|
|
|
|
|
if (!keydb) {
|
|
|
|
|
console.warn(
|
|
|
|
|
"KeyDB client is not initialized. Skipping online status.",
|
|
|
|
|
);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
// Remove the user from the Redis set
|
|
|
|
|
await keydb.srem(
|
|
|
|
|
chatKey,
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
isAdmin: ctx.session.isAdmin,
|
|
|
|
|
nome: ctx.session.nome,
|
2025-08-29 16:18:32 +02:00
|
|
|
userId: ctx.session.id,
|
2025-08-28 18:27:07 +02:00
|
|
|
}),
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const onlineUsers = await keydb.smembers(chatKey);
|
|
|
|
|
const parsedUsers = onlineUsers.map(
|
|
|
|
|
(user) => JSON.parse(user) as OnlineStatus,
|
|
|
|
|
);
|
|
|
|
|
ee.emit("chatUpdates", input.chatId, {
|
|
|
|
|
data: parsedUsers,
|
2025-08-29 16:18:32 +02:00
|
|
|
eventType: "online",
|
2025-08-28 18:27:07 +02:00
|
|
|
});
|
|
|
|
|
return parsedUsers;
|
|
|
|
|
}),
|
2025-08-29 16:18:32 +02:00
|
|
|
list: publicProcedure.query(async () => {
|
|
|
|
|
return await db
|
|
|
|
|
.selectFrom("chats")
|
|
|
|
|
.select([
|
|
|
|
|
"chats.chatid",
|
|
|
|
|
"chats.created_at",
|
|
|
|
|
"chats.user_ref",
|
|
|
|
|
getMessagesArray(),
|
|
|
|
|
])
|
|
|
|
|
.innerJoin("users", "users.id", "chats.user_ref")
|
|
|
|
|
.select(["users.username", "users.email", "users.nome"])
|
|
|
|
|
.orderBy("chats.created_at", "desc")
|
|
|
|
|
.execute();
|
|
|
|
|
}),
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
onChatEvent: protectedProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
chatId: zChatId,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.subscription(async function* ({ input, signal }) {
|
|
|
|
|
try {
|
|
|
|
|
const iterable = ee.toIterable("chatUpdates", {
|
|
|
|
|
signal,
|
|
|
|
|
});
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
for await (const [chatId, data] of iterable) {
|
|
|
|
|
if (chatId === input.chatId) {
|
|
|
|
|
yield tracked(chatId, data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (e instanceof Error && e.name === "AbortError") return;
|
|
|
|
|
console.error("Error in onChatEvent subscription:", e);
|
|
|
|
|
}
|
|
|
|
|
}),
|
2025-08-29 16:18:32 +02:00
|
|
|
|
|
|
|
|
type: protectedProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({ chatId: zChatId, typing: z.boolean(), username: z.string() }),
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
try {
|
|
|
|
|
await removeStaleTyping(input.chatId);
|
|
|
|
|
|
|
|
|
|
if (input.typing) {
|
|
|
|
|
// Add the user to the set
|
|
|
|
|
await addTyping({
|
|
|
|
|
chatId: input.chatId,
|
|
|
|
|
userId: ctx.session.id,
|
|
|
|
|
username: input.username,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
await removeTyping({
|
|
|
|
|
chatId: input.chatId,
|
|
|
|
|
userId: ctx.session.id,
|
|
|
|
|
username: input.username,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ee.emit("chatUpdates", input.chatId, {
|
|
|
|
|
data: await getCurrentlyTyping(input.chatId),
|
|
|
|
|
eventType: "typing",
|
|
|
|
|
});
|
|
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
|
|
|
message:
|
|
|
|
|
"An error occurred while processing the typing event: " +
|
|
|
|
|
(e as Error).message,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}),
|
2025-08-04 17:45:44 +02:00
|
|
|
});
|