From f2a6469a55dd8b1c40eca5a9c4d92885a2b8cf3c Mon Sep 17 00:00:00 2001 From: Artem VV Date: Fri, 19 May 2023 21:15:28 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D1=83=20=D1=87=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 45 ++++++- src/components/ChatList.astro | 31 +++++ src/components/ChatView.astro | 195 +++++++++++++++++++++++++++++++ src/components/Navbar.astro | 4 + src/db.ts | 106 ++++++++++++++++- src/pages/chatapi/createChat.ts | 37 ++++++ src/pages/chatapi/getMessages.ts | 43 +++++++ src/pages/chatapi/sendMessage.ts | 42 +++++++ src/pages/chats.astro | 48 ++++++++ src/pages/newChat.astro | 88 ++++++++++++++ 10 files changed, 632 insertions(+), 7 deletions(-) create mode 100644 src/components/ChatList.astro create mode 100644 src/components/ChatView.astro create mode 100644 src/pages/chatapi/createChat.ts create mode 100644 src/pages/chatapi/getMessages.ts create mode 100644 src/pages/chatapi/sendMessage.ts create mode 100644 src/pages/chats.astro create mode 100644 src/pages/newChat.astro diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4fb2688..7a22a2a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -42,14 +42,17 @@ model timetable { } model users { - id Int @id(map: "pk_users") @default(autoincrement()) - login String @unique @db.VarChar(25) - pass String @db.VarChar(100) - fullName String? @db.VarChar(100) - is_admin Boolean @default(false) - lore String? @db.Text() + id Int @id(map: "pk_users") @default(autoincrement()) + login String @unique @db.VarChar(25) + pass String @db.VarChar(100) + fullName String? @db.VarChar(100) + is_admin Boolean @default(false) + lore String? @db.Text() + timetable timetable[] user_session user_session[] + chat_message chat_message[] + user_in_chat user_in_chat[] } model user_session { @@ -58,3 +61,33 @@ model user_session { user users @relation(fields: [usersId], references: [id]) } + +model chat { + id String @id @default(uuid()) + title String + + chat_message chat_message[] + user_in_chat user_in_chat[] +} + +model chat_message { + id String @id @default(uuid()) + text String + + user users @relation(fields: [userId], references: [id]) + chat chat @relation(fields: [chatId], references: [id]) + + sendAt DateTime @default(now()) + + chatId String + userId Int +} + +model user_in_chat { + id Int @id @default(autoincrement()) + chatId String + userId Int + + chat chat @relation(fields: [chatId], references: [id]) + user users @relation(fields: [userId], references: [id]) +} diff --git a/src/components/ChatList.astro b/src/components/ChatList.astro new file mode 100644 index 0000000..51333cb --- /dev/null +++ b/src/components/ChatList.astro @@ -0,0 +1,31 @@ +--- +import type { users, timetable } from "@prisma/client"; +import { getUserChats } from "../db"; + +export interface Props { + user: users; +} + +const { user } = Astro.props; + +const chats = await getUserChats(user.id); +--- + +
+
    + +
    +
    + Создать чат
    +
    +
    + { + chats.map((e) => ( + +
    +
    {e.title}
    +
    +
    + )) + } +
+
diff --git a/src/components/ChatView.astro b/src/components/ChatView.astro new file mode 100644 index 0000000..b18426a --- /dev/null +++ b/src/components/ChatView.astro @@ -0,0 +1,195 @@ +--- +import type { users } from "@prisma/client"; +import type { CompositeChat } from "../db"; + +export interface Props { + user: users; + chat: CompositeChat; +} + +const { user, chat } = Astro.props; + +const formatDate = (dt: Date) => { + return ( + ("0" + dt.getDay()).substr(-2) + + "." + + ("0" + (0 + dt.getMonth())).substr(-2) + + "." + + dt.getFullYear() + + " в " + + dt.getHours() + + ":" + + ("0" + dt.getMinutes()).substr(-2) + ); +}; + +let lastMessageAt: Date | null = null; +if (chat.messages.length > 0) { + lastMessageAt = chat.messages[chat.messages.length - 1].sendAt; + lastMessageAt.setSeconds(lastMessageAt.getSeconds() + 1); +} +--- + +
+ +
+ { + chat.messages.map((e) => ( +
+
+

{e.text}

+ + {e.user.fullName ?? e.user.login} • {formatDate(e.sendAt)} + +
+
+ )) + } +
+
+ + +
+
+ + + + diff --git a/src/components/Navbar.astro b/src/components/Navbar.astro index 141cd38..b0cb174 100644 --- a/src/components/Navbar.astro +++ b/src/components/Navbar.astro @@ -18,6 +18,10 @@ const items = [ href: "/users", title: "Пользователи", }, + { + href: "/chats", + title: "Чаты", + }, ]; const itemsRight = [ diff --git a/src/db.ts b/src/db.ts index 3454859..b6be803 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1,4 +1,4 @@ -import { PrismaClient, users } from "@prisma/client"; +import { PrismaClient, chat, chat_message, users } from "@prisma/client"; import { blake3 } from "@noble/hashes/blake3"; import { bytesToHex as toHex } from "@noble/hashes/utils"; @@ -371,3 +371,107 @@ export async function updateUserTimetable(userLogin: string, oddUpdate: UpdateTT .reduce((acc, val) => acc.concat(val), []), }); } + +export async function getUserChats(userId: number) { + const chats = await client.user_in_chat.findMany({ + where: { + userId: userId, + }, + include: { + chat: true, + }, + }); + return chats.map((x) => x.chat); +} + +export type CompositeChat = { + chat: chat; + messages: (chat_message & { + user: users; + })[]; +}; + +export async function getChatMessages(chatId: string, since?: Date): Promise { + const chat = (await client.chat.findFirst({ + where: { + id: chatId, + }, + }))!; + const messages = await client.chat_message.findMany({ + where: { + chatId: chatId, + sendAt: { + gt: since, + }, + }, + include: { + user: true, + }, + orderBy: { + sendAt: "asc", + }, + }); + return { + chat, + messages, + }; +} + +export async function createChatMessage(chatId: string, userId: number, message: string) { + const msg = await client.chat_message.create({ + data: { + chatId: chatId, + userId: userId, + text: message, + }, + }); + return msg; +} + +export async function createChat(title: string, userIds: number[]) { + const chat = await client.chat.create({ + data: { + title: title, + user_in_chat: { + createMany: { + data: userIds.map((x) => { + return { + userId: x, + }; + }), + }, + }, + }, + }); + return chat; +} + +export async function userAllowedToChat(userId: number, chatId: string) { + const chat = await client.user_in_chat.findFirst({ + where: { + chatId: chatId, + userId: userId, + }, + }); + return chat !== null; +} + +export async function addUserToChat(userId: number, chatId: string) { + const chat = await client.user_in_chat.create({ + data: { + chatId: chatId, + userId: userId, + }, + }); + return chat !== null; +} + +export async function removeUserFromChat(userId: number, chatId: string) { + const chat = await client.user_in_chat.deleteMany({ + where: { + chatId: chatId, + userId: userId, + }, + }); + return chat !== null; +} diff --git a/src/pages/chatapi/createChat.ts b/src/pages/chatapi/createChat.ts new file mode 100644 index 0000000..e5e4c36 --- /dev/null +++ b/src/pages/chatapi/createChat.ts @@ -0,0 +1,37 @@ +import type { APIContext } from "astro"; +import { getSessionUser, createChat } from "../../db"; +import { Prisma } from "@prisma/client"; + +export async function post({ request, cookies }: APIContext) { + const response: { ok: boolean; reason?: string } = { + ok: true, + }; + try { + const sessId = cookies.get("session").value!; + const user = (await getSessionUser(sessId))!; + + const formData = await request.formData(); + const title = formData.get("title"); + const selectedUsers = formData.getAll("selectedUsers").map((x) => parseInt(x.toString())); + if (title === null || selectedUsers.length === 0 || selectedUsers.find((x) => isNaN(x))) { + throw new Error("Предоставлены некорректные данные для создания чата"); + } + + await createChat(title.toString(), [...selectedUsers, user.id]); + + response.ok = true; + } catch (e: any) { + response.ok = false; + if (e instanceof Prisma.PrismaClientKnownRequestError) { + response.reason = `Неизвестная ошибка базы данных. Код ${e.code}`; + } else if (e instanceof Error) { + response.reason = e.message.trim(); + } else { + response.reason = e.toString().trim(); + } + } + + return { + body: JSON.stringify(response), + }; +} diff --git a/src/pages/chatapi/getMessages.ts b/src/pages/chatapi/getMessages.ts new file mode 100644 index 0000000..9bec6f6 --- /dev/null +++ b/src/pages/chatapi/getMessages.ts @@ -0,0 +1,43 @@ +import type { APIContext } from "astro"; +import { getSessionUser, userAllowedToChat, getChatMessages, CompositeChat } from "../../db"; +import { Prisma } from "@prisma/client"; + +export async function post({ request, cookies }: APIContext) { + const response: { ok: boolean; reason?: string; data?: CompositeChat } = { + ok: true, + }; + try { + const sessId = cookies.get("session").value!; + const user = (await getSessionUser(sessId))!; + + const formData = await request.formData(); + const chatId = formData.get("chatId"); + const since = formData.get("since"); + if (chatId === null) { + throw new Error("Не предоставлены данные получения сообщений"); + } + + const userAllowed = await userAllowedToChat(user.id, chatId.toString()); + if (!userAllowed) { + throw new Error("Нет доступа к чату"); + } + + const messages = await getChatMessages(chatId.toString(), since === null ? undefined : new Date(since.toString())); + + response.ok = true; + response.data = messages; + } catch (e: any) { + response.ok = false; + if (e instanceof Prisma.PrismaClientKnownRequestError) { + response.reason = `Неизвестная ошибка базы данных. Код ${e.code}`; + } else if (e instanceof Error) { + response.reason = e.message.trim(); + } else { + response.reason = e.toString().trim(); + } + } + + return { + body: JSON.stringify(response), + }; +} diff --git a/src/pages/chatapi/sendMessage.ts b/src/pages/chatapi/sendMessage.ts new file mode 100644 index 0000000..2cf88ab --- /dev/null +++ b/src/pages/chatapi/sendMessage.ts @@ -0,0 +1,42 @@ +import type { APIContext } from "astro"; +import { getSessionUser, userAllowedToChat, createChatMessage } from "../../db"; +import { Prisma } from "@prisma/client"; + +export async function post({ request, cookies }: APIContext) { + const response: { ok: boolean; reason?: string } = { + ok: true, + }; + try { + const sessId = cookies.get("session").value!; + const user = (await getSessionUser(sessId))!; + + const formData = await request.formData(); + const chatId = formData.get("chatId"); + const message = formData.get("message"); + if (chatId === null || message === null) { + throw new Error("Не предоставлены данные отправки сообщения"); + } + + const userAllowed = await userAllowedToChat(user.id, chatId.toString()); + if (!userAllowed) { + throw new Error("Нет доступа к чату"); + } + + await createChatMessage(chatId.toString(), user.id, message.toString()); + + response.ok = true; + } catch (e: any) { + response.ok = false; + if (e instanceof Prisma.PrismaClientKnownRequestError) { + response.reason = `Неизвестная ошибка базы данных. Код ${e.code}`; + } else if (e instanceof Error) { + response.reason = e.message.trim(); + } else { + response.reason = e.toString().trim(); + } + } + + return { + body: JSON.stringify(response), + }; +} diff --git a/src/pages/chats.astro b/src/pages/chats.astro new file mode 100644 index 0000000..f612d99 --- /dev/null +++ b/src/pages/chats.astro @@ -0,0 +1,48 @@ +--- +import Layout from "../layouts/Layout.astro"; +import Navbar from "../components/Navbar.astro"; +import ChatList from "../components/ChatList.astro"; +import ChatView from "../components/ChatView.astro"; + +import { getUserSession, getSessionUser, CompositeChat, getChatMessages } from "../db"; +import type { chat, chat_message, users } from "@prisma/client"; + +if (Astro.cookies.has("session")) { + const sessId = Astro.cookies.get("session").value!; + const dbSess = await getUserSession(sessId); + if (dbSess === null) { + Astro.cookies.delete("session"); + return Astro.redirect("/login"); + } +} else { + return Astro.redirect("/login"); +} + +let openChat: CompositeChat | null = null; +if (Astro.url.searchParams.has("openChat")) { + let openChatId = Astro.url.searchParams.get("openChat")!; + openChat = await getChatMessages(openChatId); +} + +const sessId = Astro.cookies.get("session").value!; +const user = (await getSessionUser(sessId))!; +--- + + +
+ +
+ + {openChat !== null && } + { + openChat === null && ( +
+ +
+ ) + } +
+
+
diff --git a/src/pages/newChat.astro b/src/pages/newChat.astro new file mode 100644 index 0000000..0019bbb --- /dev/null +++ b/src/pages/newChat.astro @@ -0,0 +1,88 @@ +--- +import Layout from "../layouts/Layout.astro"; +import Navbar from "../components/Navbar.astro"; + +import { getUserSession, getSessionUser, searchUsers } from "../db"; + +if (Astro.cookies.has("session")) { + const sessId = Astro.cookies.get("session").value!; + const dbSess = await getUserSession(sessId); + if (dbSess === null) { + Astro.cookies.delete("session"); + return Astro.redirect("/login"); + } +} else { + return Astro.redirect("/login"); +} + +const sessId = Astro.cookies.get("session").value!; +const user = (await getSessionUser(sessId))!; + +const users = (await searchUsers({})).filter((e) => e.id !== user.id); +--- + + +
+ +
+
+
+ + +
+ +
+ { + users.map((e) => ( +
+ + +
+ )) + } +
+ +
+
+
+ + +