made pretty and fixed message posting

This commit is contained in:
Kevin Puig
2025-09-27 15:55:20 -04:00
parent 74f4e076ce
commit da1310b9c8
10 changed files with 415 additions and 390 deletions

View File

@@ -1,127 +1,126 @@
import { Context } from "hono";
import { sendMessageToChannel, removeMessageFromChannel } from "../services/realtime.js"
import {
sendMessageToChannel,
removeMessageFromChannel,
} from "../services/realtime.js";
import { success } from "zod";
export async function postMessageToChannel(io: any, c: Context) {
try {
io = c.get("io");
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 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,
);
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
});
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 deleteMessageFromChannel(
io: any,
c: Context
){
try {
export async function deleteMessageFromChannel(io: any, c: Context) {
try {
io = c.get("io");
io = c.get("io");
const instanceId = c.req.param("instanceId");
const categoryId = c.req.param("categoryId");
const channelId = c.req.param("channelId");
const messageId = c.req.param("messageId");
const instanceId = c.req.param("instanceId");
const categoryId = c.req.param("categoryId");
const channelId = c.req.param("channelId");
const messageId = c.req.param("messageId");
const result = await removeMessageFromChannel(
instanceId,
categoryId,
channelId,
messageId,
"delete_channel_message",
io,
);
const result = await removeMessageFromChannel(
instanceId,
categoryId,
channelId,
messageId,
"delete_channel_message",
io
)
if(result === "event not implemented"){
console.log("controller::realtime::deleteMessageFromChannel - Event not implemented")
return c.json({
success: false,
message: "Event not implemented or recognized",
status: 400
});
}
if(result === "no acknowledgment"){
console.log("controller::realtime::deleteMessageFromChannel - 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 delete message");
}
c.json({
success: true,
message: "Message deleted successfully",
status: 200
})
} catch (err) {
const errMessage = err as Error;
console.log("services::realtime::deleteMessageFromChannel - ", errMessage);
return c.json({
success: false,
message: errMessage.message,
status: 500
});
if (result === "event not implemented") {
console.log(
"controller::realtime::deleteMessageFromChannel - Event not implemented",
);
return c.json({
success: false,
message: "Event not implemented or recognized",
status: 400,
});
}
if (result === "no acknowledgment") {
console.log(
"controller::realtime::deleteMessageFromChannel - 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 delete message");
}
c.json({
success: true,
message: "Message deleted successfully",
status: 200,
});
} catch (err) {
const errMessage = err as Error;
console.log("services::realtime::deleteMessageFromChannel - ", errMessage);
return c.json({
success: false,
message: errMessage.message,
status: 500,
});
}
}

View File

@@ -1,4 +1,8 @@
import { getAllUsersFrom, getUserInformation, createUser } from "../services/userService";
import {
getAllUsersFrom,
getUserInformation,
createUser,
} from "../services/userService";
import { CreateUserInput } from "../validators/userValidator";
export async function fetchUserData(id: string) {

View File

@@ -1,5 +1,5 @@
import * as crypto from 'crypto';
import * as crypto from "crypto";
export default function shaHash(data:string, salt:string) : string {
return crypto.createHmac('sha256', salt).update(data).digest('hex');
export default function shaHash(data: string, salt: string): string {
return crypto.createHmac("sha256", salt).update(data).digest("hex");
}

View File

@@ -53,21 +53,19 @@ app.use(
app.route("/api", routes);
app.get(
'/openapi',
"/openapi",
openAPIRouteHandler(app, {
documentation: {
info: {
title: 'Hono API',
version: '1.0.0',
description: 'Greeting API',
title: "Hono API",
version: "1.0.0",
description: "Greeting API",
},
servers: [
{ url: 'http://localhost:3000', description: 'Local Server' },
],
servers: [{ url: "http://localhost:3000", description: "Local Server" }],
},
})
)
}),
);
app.get('/scalar', Scalar({ url: '/openapi' }))
app.get("/scalar", Scalar({ url: "/openapi" }));
export default app;

View File

@@ -1,28 +1,29 @@
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { describeRoute, resolver } from "hono-openapi";
import { postMessageToChannel,
deleteMessageFromChannel
import {
postMessageToChannel,
deleteMessageFromChannel,
} from "../controller/realtime";
const app = new Hono();
app.post(
"message/",
zValidator({
body: z.object({
content: z.string().min(1).max(500)
})
"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;
}),
async (c) => {
const { instanceId, categoryId, channelId } = c.req.params;
const { content } = c.req.body;
return postMessageToChannel(c.get("io"), {
instanceId,
categoryId,
channelId,
content
});
}
return postMessageToChannel(c.get("io"), {
instanceId,
categoryId,
channelId,
content,
});
},
);

View File

@@ -1,29 +1,38 @@
import { Hono } from "hono";
import { fetchAllUsers, fetchUserData, createNewUser } from "../controller/userController";
import { createUserSchema, queryAllUsersByInstanceId, queryUserByIdSchema } from "../validators/userValidator";
import {
fetchAllUsers,
fetchUserData,
createNewUser,
} from "../controller/userController";
import {
createUserSchema,
queryAllUsersByInstanceId,
queryUserByIdSchema,
} from "../validators/userValidator";
import { zValidator } from "@hono/zod-validator";
import { describeRoute, resolver } from "hono-openapi";
const actions = new Hono();
actions.get("user/:id",
actions.get(
"user/:id",
describeRoute({
description: "Get user by id",
responses: {
200: {
description: "Success getting user",
content: {
"application/json": { schema: resolver(queryUserByIdSchema) }
}
"application/json": { schema: resolver(queryUserByIdSchema) },
},
},
404: {
description: "User id not found",
content: {
"application/json": { schema: resolver(queryUserByIdSchema) }
}
}
}
"application/json": { schema: resolver(queryUserByIdSchema) },
},
},
},
}),
zValidator('param', queryUserByIdSchema),
zValidator("param", queryUserByIdSchema),
async (c) => {
const id = c.req.param("id");
const userData = await fetchUserData(id);
@@ -32,22 +41,23 @@ actions.get("user/:id",
} else {
return c.json({ error: "User not found" }, 404);
}
}
},
);
actions.get("user",
actions.get(
"user",
describeRoute({
description: "Get all users by instance id",
responses: {
200: {
description: "Success getting all users in instance",
content: {
"application/json": { schema: resolver(queryAllUsersByInstanceId) }
}
}
}
"application/json": { schema: resolver(queryAllUsersByInstanceId) },
},
},
},
}),
zValidator('query', queryAllUsersByInstanceId),
zValidator("query", queryAllUsersByInstanceId),
async (c) => {
const instanceId = c.req.query("instanceId");
if (!instanceId) {
@@ -60,7 +70,7 @@ actions.get("user",
} else {
return c.json({ error: "Error getting all users from instance" }, 500);
}
}
},
);
actions.post(
@@ -71,18 +81,18 @@ actions.post(
201: {
description: "Success",
content: {
'application/json': { schema: resolver(createUserSchema) },
"application/json": { schema: resolver(createUserSchema) },
},
},
400: {
description: "Bad request (user exists)",
content: {
'application/json': { schema: resolver(createUserSchema) }
}
}
}
"application/json": { schema: resolver(createUserSchema) },
},
},
},
}),
zValidator('json', createUserSchema),
zValidator("json", createUserSchema),
async (c) => {
try {
const data = await c.req.json();
@@ -94,7 +104,7 @@ actions.post(
} catch (error) {
return c.json({ error: "Error creating user" }, 500);
}
}
},
);
export default actions;

View File

@@ -3,75 +3,76 @@ import {
MessagePing,
PrismaClient,
Role,
Reply
Reply,
} from "@prisma/client";
import { CreateUserInput } from '../validators/userValidator';
import { CreateUserInput } from "../validators/userValidator";
const prisma = new PrismaClient();
class MessageService {
public async function sendMessageToChannel(
channelId: string,
userId: string,
content: string,
repliedMessageId: string | null
): Promise<{
id: string,
channelId: string,
userId: string,
text: string,
deleted: boolean,
replies: null | {
messageId: string,
repliesToId: string,
repliesToText: string
}
} | null> {
try {
const newMessage = await prisma.message.create({
data: {
channelId: channelId,
userId: userId,
text: content,
deleted: false,
}
})
let origMessage;
if(repliedMessageId){
origMessage = await prisma.message.findUnique({
where: {
id: repliedMessageId
}
})
if(!origMessage){
throw new Error("could not find original message to reply to");
}
await prisma.reply.create({
data: {
messageId: newMessage.id,
repliesToId: origMessage.id
}
})
}
return {
...newMessage,
replies: repliedMessageId ? {
messageId: newMessage.id,
repliesToId: origMessage?.id,
repliesToText: origMessage?.text
} : null
}
} catch (error) {
export async function sendMessageToChannel(
channelId: string,
userId: string,
content: string,
repliedMessageId: string | null,
): Promise<{
id: string;
channelId: string;
userId: string;
text: string;
deleted: boolean;
replies: null | {
messageId: string;
repliesToId: string;
repliesToText: string;
};
} | null> {
try {
const newMessage = await prisma.message.create({
data: {
channelId: channelId,
userId: userId,
text: content,
deleted: false,
},
});
if (!newMessage) {
return null;
}
let origMessage;
if (repliedMessageId) {
origMessage = await prisma.message.findUnique({
where: {
id: repliedMessageId,
},
});
if (!origMessage) {
throw new Error("could not find original message to reply to");
}
await prisma.reply.create({
data: {
messageId: newMessage.id,
repliesToId: origMessage.id,
},
});
}
return {
...newMessage,
channelId: newMessage.channelId!,
userId: newMessage.userId!,
replies: origMessage
? {
messageId: newMessage.id,
repliesToId: origMessage?.id,
repliesToText: origMessage?.text,
}
: null,
};
} catch (error) {
return null;
}
}

View File

@@ -1,88 +1,90 @@
import { readonly } from "zod";
const EVENTS = {
NEW_CHANNEL_MESSAGE: "new_channel_message",
DELETE_CHANNEL_MESSAGE: "delete_channel_message",
}
NEW_CHANNEL_MESSAGE: "new_channel_message",
DELETE_CHANNEL_MESSAGE: "delete_channel_message",
};
export async function sendMessageToChannel(
instanceId: string,
categoryId: string,
channelId: string,
message: any,
event: string,
io: any,
instanceId: string,
categoryId: string,
channelId: string,
message: any,
event: string,
io: any,
): Promise<string | boolean> {
try {
//TODO: implement middleware to replace this
if(EVENTS.NEW_CHANNEL_MESSAGE === event){
throw new Error("Event not implemented");
}
//TODO: add prisma to save channel message to DB
return new Promise((resolve) => {
io.to(instanceId).emit(event, message, (ack: any) => {
if (ack && ack.status === 'received') {
console.log(`Message ${ack.messageId} acknowledged by client.`);
resolve(true);
} else {
console.log('services::realtime::sendMessageToChannel No acknowledgment received from client.');
resolve("no acknowledgment");
}
});
});
} catch (err) {
const errMessage = err as Error;
if (errMessage.message === "Event not implemented") {
console.log(`services::realtime::sendMessageToChannel - Event not implemented. Attempted event: ${event}`)
return "event not implemented"
}
console.log("services::realtime::sendMessageToChannel - ", errMessage);
return false;
try {
//TODO: implement middleware to replace this
if (EVENTS.NEW_CHANNEL_MESSAGE === event) {
throw new Error("Event not implemented");
}
//TODO: add prisma to save channel message to DB
return new Promise((resolve) => {
io.to(instanceId).emit(event, message, (ack: any) => {
if (ack && ack.status === "received") {
console.log(`Message ${ack.messageId} acknowledged by client.`);
resolve(true);
} else {
console.log(
"services::realtime::sendMessageToChannel No acknowledgment received from client.",
);
resolve("no acknowledgment");
}
});
});
} catch (err) {
const errMessage = err as Error;
if (errMessage.message === "Event not implemented") {
console.log(
`services::realtime::sendMessageToChannel - Event not implemented. Attempted event: ${event}`,
);
return "event not implemented";
}
console.log("services::realtime::sendMessageToChannel - ", errMessage);
return false;
}
}
export async function removeMessageFromChannel(
instanceId: string,
categoryId: string,
channelId: string,
messageId: string,
event: string,
io: any
): Promise<string | boolean>{
try {
//TODO: implement middleware to replace this
if(EVENTS.DELETE_CHANNEL_MESSAGE === event){
throw new Error("event not implemented");
}
//TODO: add prisma to flag a channel message as deleted
return new Promise((resolve) => {
io.to(instanceId).emit(event, { messageId }, (ack: any) => {
if (ack && ack.status === 'received') {
console.log(`Message ${ack.messageId} acknowledged by client.`);
resolve(true);
} else {
console.log('services::realtime::deleteMessageFromChannel No acknowledgment received from client.');
resolve("no acknowledgment");
}
});
});
} catch (err) {
const errMessage = err as Error;
if (errMessage.message === "Event not implemented") {
console.log(`services::realtime::deleteMessageFromChannel - Event not implemented. Attempted event: ${event}`)
return false;
}
console.log("services::realtime::deleteMessageFromChannel - ", errMessage);
return false;
instanceId: string,
categoryId: string,
channelId: string,
messageId: string,
event: string,
io: any,
): Promise<string | boolean> {
try {
//TODO: implement middleware to replace this
if (EVENTS.DELETE_CHANNEL_MESSAGE === event) {
throw new Error("event not implemented");
}
//TODO: add prisma to flag a channel message as deleted
return new Promise((resolve) => {
io.to(instanceId).emit(event, { messageId }, (ack: any) => {
if (ack && ack.status === "received") {
console.log(`Message ${ack.messageId} acknowledged by client.`);
resolve(true);
} else {
console.log(
"services::realtime::deleteMessageFromChannel No acknowledgment received from client.",
);
resolve("no acknowledgment");
}
});
});
} catch (err) {
const errMessage = err as Error;
if (errMessage.message === "Event not implemented") {
console.log(
`services::realtime::deleteMessageFromChannel - Event not implemented. Attempted event: ${event}`,
);
return false;
}
console.log("services::realtime::deleteMessageFromChannel - ", errMessage);
return false;
}
}

View File

@@ -5,31 +5,35 @@ import {
Role,
UserAuth,
} from "@prisma/client";
import { CreateUserInput } from '../validators/userValidator';
import { CreateUserInput } from "../validators/userValidator";
import shaHash from "../helper/hashing";
const prisma = new PrismaClient();
export async function createUser(data: CreateUserInput): Promise<{
username: string,
nickname: string | null,
bio: string | null,
picture: string | null,
banner: string | null,
status: string,
admin: boolean
username: string;
nickname: string | null;
bio: string | null;
picture: string | null;
banner: string | null;
status: string;
admin: boolean;
} | null> {
const requestingUser = await getUserInformation(data.requestingUserId);
const requestingUserCredentials = await getUserCredentials(data.requestingUserId)
if (!requestingUser
|| !requestingUserCredentials
|| !requestingUser.admin
|| requestingUserCredentials.token == null
|| data.requestingUserToken != requestingUserCredentials.token) {
const requestingUserCredentials = await getUserCredentials(
data.requestingUserId,
);
if (
!requestingUser ||
!requestingUserCredentials ||
!requestingUser.admin ||
requestingUserCredentials.token == null ||
data.requestingUserToken != requestingUserCredentials.token
) {
return null;
}
if (await prisma.user.count({ where: { username: data.username }}) >= 1) {
if ((await prisma.user.count({ where: { username: data.username } })) >= 1) {
return null;
}
@@ -45,13 +49,15 @@ export async function createUser(data: CreateUserInput): Promise<{
},
});
if (!(await prisma.userAuth.create({
data: {
userId: userData.id,
password: shaHash(data.passwordhash, userData.id),
token: null,
}
}))) {
if (
!(await prisma.userAuth.create({
data: {
userId: userData.id,
password: shaHash(data.passwordhash, userData.id),
token: null,
},
}))
) {
return null;
}
@@ -59,52 +65,52 @@ export async function createUser(data: CreateUserInput): Promise<{
}
export async function getUserCredentials(userId: string): Promise<{
userId: string,
password: string,
token: string | null
} | null> {
try {
if (!userId) {
throw new Error("missing userId");
}
userId: string;
password: string;
token: string | null;
} | null> {
try {
if (!userId) {
throw new Error("missing userId");
}
const userAuth = await prisma.userAuth.findUnique({
where: {
userId: userId,
},
});
const userAuth = await prisma.userAuth.findUnique({
where: {
userId: userId,
},
});
if (!userAuth) {
throw new Error("could not find user credentials");
}
if (!userAuth) {
throw new Error("could not find user credentials");
}
return {
userId: userAuth.userId,
password: userAuth.password,
token: userAuth.token,
};
} catch (err) {
const errMessage = err as Error;
return {
userId: userAuth.userId,
password: userAuth.password,
token: userAuth.token,
};
} catch (err) {
const errMessage = err as Error;
if (errMessage.message === "missing userId") {
console.log("services::actions::getUserCredentials - missing userId");
return null;
}
if (errMessage.message === "could not find user credentials") {
console.log(
"services::actions::getUserCredentials - unable to find user credentials",
);
return null;
}
if (errMessage.message === "missing userId") {
console.log("services::actions::getUserCredentials - missing userId");
return null;
}
if (errMessage.message === "could not find user credentials") {
console.log(
"services::actions::getUserCredentials - unknown error",
errMessage,
"services::actions::getUserCredentials - unable to find user credentials",
);
return null;
}
console.log(
"services::actions::getUserCredentials - unknown error",
errMessage,
);
return null;
}
}
export async function getUserInformation(userId: string): Promise<{
id: string;
@@ -196,9 +202,9 @@ export async function getAllUsersFrom(instanceId: string): Promise<
try {
const instances = await prisma.instance.count({
where: {
id: instanceId
}
})
id: instanceId,
},
});
if (instances < 1) {
throw new Error("could not find given instance id");
}
@@ -218,8 +224,8 @@ export async function getAllUsersFrom(instanceId: string): Promise<
const admins = await prisma.user.findMany({
where: {
admin: true
}
admin: true,
},
});
if (!admins) {
throw new Error("could not get all admins");
@@ -249,13 +255,13 @@ export async function getAllUsersFrom(instanceId: string): Promise<
).includes(u.status as any)
? (u.status as "online" | "offline" | "dnd" | "idle" | "invis")
: "offline",
role: adminRoles.map(r => ({
role: adminRoles.map((r) => ({
userId: r.userId,
instanceId: r.instanceId,
})),
}
})
)
};
}),
);
const userData = await Promise.all(
users.map(async (u) => {

View File

@@ -1,12 +1,12 @@
import { z } from 'zod'
import { z } from "zod";
export const queryUserByIdSchema = z.object({
id: z.uuidv7()
})
id: z.uuidv7(),
});
export const queryAllUsersByInstanceId = z.object({
instanceId: z.uuidv7()
})
instanceId: z.uuidv7(),
});
export const createUserSchema = z.object({
username: z.string().min(3).max(30),
@@ -14,13 +14,17 @@ export const createUserSchema = z.object({
bio: z.string().max(500).optional(),
picture: z.url().optional(),
banner: z.url().optional(),
status: z.enum(['online', 'offline', 'dnd', 'idle', 'invis']).default('online'),
status: z
.enum(["online", "offline", "dnd", "idle", "invis"])
.default("online"),
admin: z.boolean().default(false),
requestingUserId: z.uuidv7(),
requestingUserToken: z.uuidv4(),
passwordhash: z.string(),
})
});
export type QueryUserByIdInput = z.infer<typeof queryUserByIdSchema>
export type QueryAllUsersByInstanceIdInput = z.infer<typeof queryAllUsersByInstanceId>
export type CreateUserInput = z.infer<typeof createUserSchema>
export type QueryUserByIdInput = z.infer<typeof queryUserByIdSchema>;
export type QueryAllUsersByInstanceIdInput = z.infer<
typeof queryAllUsersByInstanceId
>;
export type CreateUserInput = z.infer<typeof createUserSchema>;