Compare commits
1 Commits
staging
...
chat-syste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0c22453f1 |
10
prisma/migrations/20250429143447_log/migration.sql
Normal file
10
prisma/migrations/20250429143447_log/migration.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Log" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"level" TEXT NOT NULL,
|
||||
"message" TEXT NOT NULL,
|
||||
"meta" JSONB NOT NULL,
|
||||
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Log_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
64
prisma/migrations/20250429145532_messages/migration.sql
Normal file
64
prisma/migrations/20250429145532_messages/migration.sql
Normal file
@@ -0,0 +1,64 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Attachment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"kategorie" TEXT,
|
||||
"mime" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Attachment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Message" (
|
||||
"id" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"sender_id" TEXT NOT NULL,
|
||||
"reply_to_id" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_AttachmentToMessage" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "_AttachmentToMessage_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_recipients" (
|
||||
"A" VARCHAR(11) NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "_recipients_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Attachment_id_key" ON "Attachment"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_AttachmentToMessage_B_index" ON "_AttachmentToMessage"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_recipients_B_index" ON "_recipients"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_sender_id_fkey" FOREIGN KEY ("sender_id") REFERENCES "benutzer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_reply_to_id_fkey" FOREIGN KEY ("reply_to_id") REFERENCES "Message"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AttachmentToMessage" ADD CONSTRAINT "_AttachmentToMessage_A_fkey" FOREIGN KEY ("A") REFERENCES "Attachment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AttachmentToMessage" ADD CONSTRAINT "_AttachmentToMessage_B_fkey" FOREIGN KEY ("B") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_recipients" ADD CONSTRAINT "_recipients_A_fkey" FOREIGN KEY ("A") REFERENCES "benutzer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_recipients" ADD CONSTRAINT "_recipients_B_fkey" FOREIGN KEY ("B") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
56
prisma/migrations/20250429151551_conversations/migration.sql
Normal file
56
prisma/migrations/20250429151551_conversations/migration.sql
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `created_at` on the `Message` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `updated_at` on the `Message` table. All the data in the column will be lost.
|
||||
- You are about to drop the `_recipients` table. If the table is not empty, all the data it contains will be lost.
|
||||
- Added the required column `conversation_id` to the `Message` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "_recipients" DROP CONSTRAINT "_recipients_A_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "_recipients" DROP CONSTRAINT "_recipients_B_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Message" DROP COLUMN "created_at",
|
||||
DROP COLUMN "updated_at",
|
||||
ADD COLUMN "conversation_id" TEXT NOT NULL,
|
||||
ADD COLUMN "sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "_recipients";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Conversation" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Participant" (
|
||||
"id" TEXT NOT NULL,
|
||||
"benutzer_id" TEXT NOT NULL,
|
||||
"conversation_id" TEXT NOT NULL,
|
||||
"joined_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Participant_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Conversation_id_key" ON "Conversation"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Participant_benutzer_id_conversation_id_key" ON "Participant"("benutzer_id", "conversation_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_conversation_id_fkey" FOREIGN KEY ("conversation_id") REFERENCES "Conversation"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Participant" ADD CONSTRAINT "Participant_benutzer_id_fkey" FOREIGN KEY ("benutzer_id") REFERENCES "benutzer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Participant" ADD CONSTRAINT "Participant_conversation_id_fkey" FOREIGN KEY ("conversation_id") REFERENCES "Conversation"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
9
prisma/schema/Attachment.prisma
Normal file
9
prisma/schema/Attachment.prisma
Normal file
@@ -0,0 +1,9 @@
|
||||
model Attachment {
|
||||
id String @id @unique @default(uuid())
|
||||
|
||||
name String?
|
||||
kategorie String?
|
||||
mime String
|
||||
|
||||
attached_to_messages Message[]
|
||||
}
|
||||
@@ -49,7 +49,10 @@ model Benutzer {
|
||||
BearbeiteteTickets Tickets[] @relation("BearbeiteteTickets")
|
||||
events Event[]
|
||||
|
||||
@@map("benutzer")
|
||||
conversations Participant[]
|
||||
messages Message[]
|
||||
|
||||
@@map("benutzer")
|
||||
}
|
||||
|
||||
|
||||
|
||||
9
prisma/schema/Conversation.prisma
Normal file
9
prisma/schema/Conversation.prisma
Normal file
@@ -0,0 +1,9 @@
|
||||
model Conversation {
|
||||
id String @unique @default(cuid())
|
||||
|
||||
name String?
|
||||
participants Participant[]
|
||||
messages Message[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
7
prisma/schema/Log.prisma
Normal file
7
prisma/schema/Log.prisma
Normal file
@@ -0,0 +1,7 @@
|
||||
model Log {
|
||||
id Int @id @default(autoincrement())
|
||||
level String
|
||||
message String
|
||||
meta Json
|
||||
timestamp DateTime @default(now())
|
||||
}
|
||||
17
prisma/schema/Message.prisma
Normal file
17
prisma/schema/Message.prisma
Normal file
@@ -0,0 +1,17 @@
|
||||
model Message {
|
||||
id String @id @default(cuid())
|
||||
|
||||
attachments Attachment[]
|
||||
|
||||
reply_to_id String? // Nullable because not all messages are replies
|
||||
reply_to Message? @relation("MessageReplies", fields: [reply_to_id], references: [id])
|
||||
replies Message[] @relation("MessageReplies")
|
||||
|
||||
content String
|
||||
sender_id String
|
||||
conversation_id String
|
||||
sentAt DateTime @default(now())
|
||||
|
||||
sender Benutzer @relation(fields: [sender_id], references: [id])
|
||||
conversation Conversation @relation(fields: [conversation_id], references: [id])
|
||||
}
|
||||
11
prisma/schema/Participant.prisma
Normal file
11
prisma/schema/Participant.prisma
Normal file
@@ -0,0 +1,11 @@
|
||||
model Participant {
|
||||
id String @id @default(cuid())
|
||||
benutzer_id String
|
||||
conversation_id String
|
||||
joined_at DateTime @default(now())
|
||||
|
||||
benutzer Benutzer @relation(fields: [benutzer_id], references: [id])
|
||||
conversation Conversation @relation(fields: [conversation_id], references: [id])
|
||||
|
||||
@@unique([benutzer_id, conversation_id])
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import express from 'express';
|
||||
import { H, Handlers } from '@highlight-run/node'
|
||||
// @ts-ignore
|
||||
import { handler as ssrHandler } from './dist/server/entry.mjs';
|
||||
import { server } from "./src/ws/server";
|
||||
|
||||
const highlightConfig = {
|
||||
projectID: '1jdkoe52',
|
||||
|
||||
8
src/generated/zod/attachment.ts
Normal file
8
src/generated/zod/attachment.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as z from "zod"
|
||||
|
||||
export const AttachmentSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullish(),
|
||||
kategorie: z.string().nullish(),
|
||||
mime: z.string(),
|
||||
})
|
||||
8
src/generated/zod/conversation.ts
Normal file
8
src/generated/zod/conversation.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as z from "zod"
|
||||
|
||||
export const ConversationSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullish(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
})
|
||||
@@ -1,16 +1,21 @@
|
||||
export * from "./anteilshaber"
|
||||
export * from "./apirequests"
|
||||
export * from "./attachment"
|
||||
export * from "./aufnahme"
|
||||
export * from "./bedarfsausweisgewerbe"
|
||||
export * from "./bedarfsausweiswohnen"
|
||||
export * from "./benutzer"
|
||||
export * from "./bild"
|
||||
export * from "./conversation"
|
||||
export * from "./event"
|
||||
export * from "./gegeinpreisung"
|
||||
export * from "./gegnachweisgewerbe"
|
||||
export * from "./gegnachweiswohnen"
|
||||
export * from "./klimafaktoren"
|
||||
export * from "./log"
|
||||
export * from "./message"
|
||||
export * from "./objekt"
|
||||
export * from "./participant"
|
||||
export * from "./postleitzahlen"
|
||||
export * from "./rechnung"
|
||||
export * from "./refreshtokens"
|
||||
|
||||
15
src/generated/zod/log.ts
Normal file
15
src/generated/zod/log.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as z from "zod"
|
||||
|
||||
// Helper schema for JSON fields
|
||||
type Literal = boolean | number | string
|
||||
type Json = Literal | { [key: string]: Json } | Json[]
|
||||
const literalSchema = z.union([z.string(), z.number(), z.boolean()])
|
||||
const jsonSchema: z.ZodSchema<Json> = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]))
|
||||
|
||||
export const LogSchema = z.object({
|
||||
id: z.number().int(),
|
||||
level: z.string(),
|
||||
message: z.string(),
|
||||
meta: jsonSchema,
|
||||
timestamp: z.date(),
|
||||
})
|
||||
10
src/generated/zod/message.ts
Normal file
10
src/generated/zod/message.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as z from "zod"
|
||||
|
||||
export const MessageSchema = z.object({
|
||||
id: z.string(),
|
||||
reply_to_id: z.string().nullish(),
|
||||
content: z.string(),
|
||||
sender_id: z.string(),
|
||||
conversation_id: z.string(),
|
||||
sentAt: z.date(),
|
||||
})
|
||||
8
src/generated/zod/participant.ts
Normal file
8
src/generated/zod/participant.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as z from "zod"
|
||||
|
||||
export const ParticipantSchema = z.object({
|
||||
id: z.string(),
|
||||
benutzer_id: z.string(),
|
||||
conversation_id: z.string(),
|
||||
joined_at: z.date(),
|
||||
})
|
||||
18
src/modules/ChatModule.svelte
Normal file
18
src/modules/ChatModule.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
const socket = new WebSocket("http://localhost:8080");
|
||||
|
||||
socket.on("open", () => {
|
||||
console.log("Connected to WebSocket");
|
||||
|
||||
socket.send("Hello")
|
||||
})
|
||||
|
||||
socket.on("message", (data) => {
|
||||
console.log(data);
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
||||
</div>
|
||||
8
src/pages/chat.astro
Normal file
8
src/pages/chat.astro
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import Layout from "#layouts/Layout.astro";
|
||||
import ChatModule from "#modules/ChatModule.svelte";
|
||||
---
|
||||
|
||||
<Layout title="Websockets">
|
||||
<ChatModule client:only></ChatModule>
|
||||
</Layout>
|
||||
36
src/server/logger.ts
Normal file
36
src/server/logger.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { prisma } from "#lib/server/prisma.js";
|
||||
import winston from "winston";
|
||||
import Transport from "winston-transport";
|
||||
|
||||
class DatabaseTransport extends Transport {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async log(info: any, callback: () => void) {
|
||||
setImmediate(() => {
|
||||
this.emit("logged", info);
|
||||
});
|
||||
|
||||
const { level, message, ...meta } = info;
|
||||
|
||||
await prisma.log.create({
|
||||
data: {
|
||||
level,
|
||||
message,
|
||||
meta: JSON.stringify(meta),
|
||||
},
|
||||
});
|
||||
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: "info",
|
||||
format: winston.format.json(),
|
||||
transports: [new DatabaseTransport(), new winston.transports.Console({
|
||||
handleExceptions: true,
|
||||
handleRejections: true,
|
||||
})]
|
||||
});
|
||||
30
src/ws/server.ts
Normal file
30
src/ws/server.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { logger } from '#server/logger.js';
|
||||
import { WebSocketServer } from 'ws';
|
||||
|
||||
const server = new WebSocketServer({ port: 8080 });
|
||||
|
||||
|
||||
|
||||
server.on('connection', (ws, req) => {
|
||||
logger.info("Client connected to websocket", {
|
||||
ip: req.socket.remoteAddress
|
||||
})
|
||||
|
||||
ws.on('message', (message) => {
|
||||
logger.info("Received websocket message", {
|
||||
message
|
||||
});
|
||||
|
||||
ws.send(`Server received: ${message}`);
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
logger.info('Client disconnected', {
|
||||
ip: req.socket.remoteAddress
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
logger.info(`Websocket server listening on port ${8080}`)
|
||||
|
||||
export { server };
|
||||
60
src/ws/types.ts
Normal file
60
src/ws/types.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Conversation, Message } from "#lib/client/prisma.js";
|
||||
|
||||
export type WebsocketClientToServerMessage =
|
||||
| {
|
||||
type: "messages/get";
|
||||
payload: {
|
||||
conversation_id: string;
|
||||
filters?: {
|
||||
after?: Date;
|
||||
before?: Date;
|
||||
};
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "message/send";
|
||||
payload: {
|
||||
conversation_id: string;
|
||||
content: string;
|
||||
reply_to_id?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "conversations/get";
|
||||
payload: {
|
||||
includeMessages?: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "conversation/create";
|
||||
payload: {
|
||||
participant_ids: string[]; // includes sender
|
||||
name?: string; // optional for group
|
||||
};
|
||||
};
|
||||
|
||||
/* ---------------------------- Server to client ---------------------------- */
|
||||
|
||||
export type WebsocketServerToClientMessage =
|
||||
| {
|
||||
type: "messages/list";
|
||||
payload: {
|
||||
conversation_id: string;
|
||||
messages: Message[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "message/new";
|
||||
payload: Message;
|
||||
}
|
||||
| {
|
||||
type: "conversations/list";
|
||||
payload: Conversation[];
|
||||
}
|
||||
| {
|
||||
type: "error";
|
||||
payload: {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user