Add messaging
This commit is contained in:
parent
23128f25e1
commit
5733975aa0
29 changed files with 986 additions and 8 deletions
138
package-lock.json
generated
138
package-lock.json
generated
|
|
@ -1,28 +1,31 @@
|
|||
{
|
||||
"name": "tether",
|
||||
"version": "0.3.7",
|
||||
"version": "0.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tether",
|
||||
"version": "0.3.7",
|
||||
"version": "0.4.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/cors": "^11.2.0",
|
||||
"@fastify/websocket": "^11.2.0",
|
||||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"argon2": "^0.44.0",
|
||||
"fastify": "^5.6.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"pg": "^8.16.3",
|
||||
"uuid": "^13.0.0"
|
||||
"uuid": "^13.0.0",
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/pg": "^8.16.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"prisma": "^7.2.0",
|
||||
"ts-node": "^10.9.2",
|
||||
|
|
@ -266,6 +269,27 @@
|
|||
"ipaddr.js": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/websocket": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-11.2.0.tgz",
|
||||
"integrity": "sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"duplexify": "^4.1.3",
|
||||
"fastify-plugin": "^5.0.0",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz",
|
||||
|
|
@ -606,6 +630,16 @@
|
|||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/abstract-logging": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
||||
|
|
@ -954,6 +988,18 @@
|
|||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexify": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
|
||||
"integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.4.1",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1",
|
||||
"stream-shift": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
|
|
@ -984,6 +1030,15 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||
|
|
@ -1247,6 +1302,12 @@
|
|||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
|
||||
|
|
@ -1576,6 +1637,15 @@
|
|||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
|
|
@ -1927,6 +1997,20 @@
|
|||
"react": "^19.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
|
|
@ -2183,6 +2267,21 @@
|
|||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stream-shift": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
|
||||
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/thread-stream": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
|
||||
|
|
@ -2289,6 +2388,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||
|
|
@ -2339,6 +2444,33 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "tether",
|
||||
"version": "0.3.7",
|
||||
"version": "0.4.0",
|
||||
"description": "Communication server using the Nexlink protocol",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/pg": "^8.16.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"prisma": "^7.2.0",
|
||||
"ts-node": "^10.9.2",
|
||||
|
|
@ -27,12 +28,14 @@
|
|||
"dependencies": {
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/cors": "^11.2.0",
|
||||
"@fastify/websocket": "^11.2.0",
|
||||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"argon2": "^0.44.0",
|
||||
"fastify": "^5.6.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"pg": "^8.16.3",
|
||||
"uuid": "^13.0.0"
|
||||
"uuid": "^13.0.0",
|
||||
"ws": "^8.19.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
prisma/migrations/20260111121740_message/migration.sql
Normal file
21
prisma/migrations/20260111121740_message/migration.sql
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "Message" (
|
||||
"id" TEXT NOT NULL,
|
||||
"text" TEXT NOT NULL,
|
||||
"editHistory" TEXT[],
|
||||
"edited" BOOLEAN NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"channelId" TEXT NOT NULL,
|
||||
"creationDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Message_id_key" ON "Message"("id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
3
prisma/migrations/20260111124811_message_1/migration.sql
Normal file
3
prisma/migrations/20260111124811_message_1/migration.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Message" ALTER COLUMN "editHistory" SET DEFAULT ARRAY[]::TEXT[],
|
||||
ALTER COLUMN "edited" SET DEFAULT false;
|
||||
16
prisma/migrations/20260111125054_message_2/migration.sql
Normal file
16
prisma/migrations/20260111125054_message_2/migration.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `userId` on the `Message` table. All the data in the column will be lost.
|
||||
- Added the required column `ownerId` to the `Message` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Message" DROP CONSTRAINT "Message_userId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Message" DROP COLUMN "userId",
|
||||
ADD COLUMN "ownerId" TEXT NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
|
@ -28,6 +28,7 @@ model Channel {
|
|||
community Community @relation(fields: [communityId], references: [id], onDelete: Cascade)
|
||||
communityId String
|
||||
creationDate DateTime @default(now())
|
||||
messages Message[]
|
||||
}
|
||||
|
||||
model Role {
|
||||
|
|
@ -55,6 +56,7 @@ model User {
|
|||
ownedCommunities Community[] @relation(name: "OwnerCommunityToUser")
|
||||
communities Community[] @relation(name: "MembersCommunitiesToUsers")
|
||||
roles Role[] @relation(name: "UsersRolesToUsers")
|
||||
messages Message[]
|
||||
}
|
||||
|
||||
model Session {
|
||||
|
|
@ -76,3 +78,15 @@ model Invite {
|
|||
creationDate DateTime @default(now())
|
||||
expirationDate DateTime?
|
||||
}
|
||||
|
||||
model Message {
|
||||
id String @id @unique @default(uuid())
|
||||
text String
|
||||
editHistory String[] @default([])
|
||||
edited Boolean @default(false)
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
ownerId String
|
||||
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
||||
channelId String
|
||||
creationDate DateTime @default(now())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,16 @@ import type {
|
|||
IDeleteChannelParams,
|
||||
IDeleteChannelResponseError,
|
||||
IDeleteChannelResponseSuccess,
|
||||
IGetMessagesParams,
|
||||
IGetMessagesResponseError,
|
||||
IGetMessagesResponseSuccess,
|
||||
} from "./types.js";
|
||||
import {
|
||||
createChannelAuth,
|
||||
deleteChannelByIdAuth,
|
||||
getChannelByIdAuth,
|
||||
updateChannelByIdAuth,
|
||||
getChannelMessagesByIdAuth,
|
||||
} from "../../services/channel/channel.js";
|
||||
import { API_ERROR } from "../errors.js";
|
||||
|
||||
|
|
@ -135,4 +139,42 @@ const deleteChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IDeleteChannelResponseSuccess;
|
||||
};
|
||||
|
||||
export { getChannel, postCreateChannel, patchChannel, deleteChannel };
|
||||
const getMessages = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { id } = request.params as IGetMessagesParams;
|
||||
const authHeader = request.headers["authorization"];
|
||||
|
||||
const messages = await getChannelMessagesByIdAuth(id, authHeader);
|
||||
if (!messages) {
|
||||
reply.status(404);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.NOT_FOUND,
|
||||
} as IGetMessagesResponseError;
|
||||
}
|
||||
if (messages === API_ERROR.ACCESS_DENIED) {
|
||||
reply.status(403);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.ACCESS_DENIED,
|
||||
} as IGetMessagesResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: id,
|
||||
messages: messages.map((message) => ({
|
||||
id: message.id,
|
||||
text: message.text,
|
||||
edited: message.edited,
|
||||
ownerId: message.ownerId,
|
||||
creationDate: message.creationDate.getTime(),
|
||||
})),
|
||||
} as IGetMessagesResponseSuccess;
|
||||
};
|
||||
|
||||
export {
|
||||
getChannel,
|
||||
postCreateChannel,
|
||||
patchChannel,
|
||||
deleteChannel,
|
||||
getMessages,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const channelRoutes = async (fastify: FastifyInstance) => {
|
|||
fastify.post(`/`, controller.postCreateChannel);
|
||||
fastify.patch(`/:id`, controller.patchChannel);
|
||||
fastify.delete(`/:id`, controller.deleteChannel);
|
||||
fastify.get(`/:id/messages`, controller.getMessages);
|
||||
};
|
||||
|
||||
export { channelRoutes };
|
||||
|
|
|
|||
|
|
@ -62,6 +62,28 @@ interface IDeleteChannelResponseSuccess {
|
|||
communityId: string;
|
||||
}
|
||||
|
||||
interface IGetMessagesParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface IGetMessagesResponseError {
|
||||
id: string;
|
||||
error: API_ERROR;
|
||||
}
|
||||
|
||||
interface IGetMessagesResponseSuccess {
|
||||
id: string;
|
||||
messages: IGetMessagesResponseMessage[];
|
||||
}
|
||||
|
||||
interface IGetMessagesResponseMessage {
|
||||
id: string;
|
||||
text: string;
|
||||
edited: boolean;
|
||||
ownerId: string;
|
||||
creationDate: number;
|
||||
}
|
||||
|
||||
export {
|
||||
type IChannel,
|
||||
type IGetChannelParams,
|
||||
|
|
@ -77,4 +99,8 @@ export {
|
|||
type IDeleteChannelParams,
|
||||
type IDeleteChannelResponseError,
|
||||
type IDeleteChannelResponseSuccess,
|
||||
type IGetMessagesParams,
|
||||
type IGetMessagesResponseError,
|
||||
type IGetMessagesResponseSuccess,
|
||||
type IGetMessagesResponseMessage,
|
||||
};
|
||||
|
|
|
|||
3
src/controllers/message/index.ts
Normal file
3
src/controllers/message/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./message.js";
|
||||
export * from "./routes.js";
|
||||
export * from "./types.js";
|
||||
145
src/controllers/message/message.ts
Normal file
145
src/controllers/message/message.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { type FastifyReply, type FastifyRequest } from "fastify";
|
||||
import type {
|
||||
IGetMessageParams,
|
||||
IGetMessageResponseError,
|
||||
IGetMessageResponseSuccess,
|
||||
IPostCreateMessageRequest,
|
||||
IPostCreateMessageResponseError,
|
||||
IPostCreateMessageResponseSuccess,
|
||||
IPatchMessageParams,
|
||||
IPatchMessageRequest,
|
||||
IPatchMessageResponseError,
|
||||
IPatchMessageResponseSuccess,
|
||||
IDeleteMessageParams,
|
||||
IDeleteMessageResponseError,
|
||||
IDeleteMessageResponseSuccess,
|
||||
} from "./types.js";
|
||||
import {
|
||||
createMessageAuth,
|
||||
deleteMessageByIdAuth,
|
||||
getMessageByIdAuth,
|
||||
updateMessageByIdAuth,
|
||||
} from "../../services/message/message.js";
|
||||
import { API_ERROR } from "../errors.js";
|
||||
|
||||
const getMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { id } = request.params as IGetMessageParams;
|
||||
const authHeader = request.headers["authorization"];
|
||||
|
||||
const message = await getMessageByIdAuth(id, authHeader);
|
||||
if (!message) {
|
||||
reply.status(404);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.NOT_FOUND,
|
||||
} as IGetMessageResponseError;
|
||||
}
|
||||
if (message === API_ERROR.ACCESS_DENIED) {
|
||||
reply.status(403);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.ACCESS_DENIED,
|
||||
} as IGetMessageResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
text: message.text,
|
||||
editHistory: message.editHistory,
|
||||
edited: message.edited,
|
||||
ownerId: message.ownerId,
|
||||
channelId: message.channelId,
|
||||
creationDate: message.creationDate.getTime(),
|
||||
} as IGetMessageResponseSuccess;
|
||||
};
|
||||
|
||||
const postCreateMessage = async (
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
const createMessageRequest = request.body as IPostCreateMessageRequest;
|
||||
const authHeader = request.headers["authorization"];
|
||||
|
||||
const message = await createMessageAuth(createMessageRequest, authHeader);
|
||||
if (message === API_ERROR.ACCESS_DENIED) {
|
||||
reply.status(403);
|
||||
return {
|
||||
error: API_ERROR.ACCESS_DENIED,
|
||||
} as IPostCreateMessageResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
text: message.text,
|
||||
editHistory: message.editHistory,
|
||||
edited: message.edited,
|
||||
ownerId: message.ownerId,
|
||||
channelId: message.channelId,
|
||||
creationDate: message.creationDate.getTime(),
|
||||
} as IPostCreateMessageResponseSuccess;
|
||||
};
|
||||
|
||||
const patchMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { id } = request.params as IPatchMessageParams;
|
||||
const patchMessageRequest = request.body as IPatchMessageRequest;
|
||||
const authHeader = request.headers["authorization"];
|
||||
|
||||
const message = await updateMessageByIdAuth(
|
||||
id,
|
||||
patchMessageRequest,
|
||||
authHeader,
|
||||
);
|
||||
if (!message) {
|
||||
reply.status(404);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.NOT_FOUND,
|
||||
} as IPatchMessageResponseError;
|
||||
}
|
||||
if (message === API_ERROR.ACCESS_DENIED) {
|
||||
reply.status(403);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.ACCESS_DENIED,
|
||||
} as IPatchMessageResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
text: message.text,
|
||||
editHistory: message.editHistory,
|
||||
edited: message.edited,
|
||||
ownerId: message.ownerId,
|
||||
channelId: message.channelId,
|
||||
creationDate: message.creationDate.getTime(),
|
||||
} as IPatchMessageResponseSuccess;
|
||||
};
|
||||
|
||||
const deleteMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { id } = request.params as IDeleteMessageParams;
|
||||
const authHeader = request.headers["authorization"];
|
||||
|
||||
const message = await deleteMessageByIdAuth(id, authHeader);
|
||||
if (!message) {
|
||||
reply.status(404);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.NOT_FOUND,
|
||||
} as IDeleteMessageResponseError;
|
||||
}
|
||||
if (message === API_ERROR.ACCESS_DENIED) {
|
||||
reply.status(403);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.ACCESS_DENIED,
|
||||
} as IDeleteMessageResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
ownerId: message.ownerId,
|
||||
channelId: message.channelId,
|
||||
} as IDeleteMessageResponseSuccess;
|
||||
};
|
||||
|
||||
export { getMessage, postCreateMessage, patchMessage, deleteMessage };
|
||||
11
src/controllers/message/routes.ts
Normal file
11
src/controllers/message/routes.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { type FastifyInstance } from "fastify";
|
||||
import * as controller from "./message.js";
|
||||
|
||||
const messageRoutes = async (fastify: FastifyInstance) => {
|
||||
fastify.get(`/:id`, controller.getMessage);
|
||||
fastify.post(`/`, controller.postCreateMessage);
|
||||
fastify.patch(`/:id`, controller.patchMessage);
|
||||
fastify.delete(`/:id`, controller.deleteMessage);
|
||||
};
|
||||
|
||||
export { messageRoutes };
|
||||
81
src/controllers/message/types.ts
Normal file
81
src/controllers/message/types.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import type { API_ERROR } from "../errors.js";
|
||||
|
||||
interface IMessage {
|
||||
id: string;
|
||||
text: string;
|
||||
editHistory: string[];
|
||||
edited: boolean;
|
||||
ownerId: string;
|
||||
channelId: string;
|
||||
creationDate: number;
|
||||
}
|
||||
|
||||
interface IGetMessageParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface IGetMessageResponseError {
|
||||
id: string;
|
||||
error: API_ERROR;
|
||||
}
|
||||
|
||||
interface IGetMessageResponseSuccess extends IMessage {}
|
||||
|
||||
interface IPostCreateMessageRequest {
|
||||
text: string;
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
interface IPostCreateMessageResponseError {
|
||||
id: string;
|
||||
error: API_ERROR;
|
||||
}
|
||||
|
||||
interface IPostCreateMessageResponseSuccess extends IMessage {}
|
||||
|
||||
interface IPatchMessageParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface IPatchMessageRequest {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface IPatchMessageResponseError {
|
||||
id: string;
|
||||
error: API_ERROR;
|
||||
}
|
||||
|
||||
interface IPatchMessageResponseSuccess extends IMessage {}
|
||||
|
||||
interface IDeleteMessageParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface IDeleteMessageResponseError {
|
||||
id: string;
|
||||
error: API_ERROR;
|
||||
}
|
||||
|
||||
interface IDeleteMessageResponseSuccess {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
export {
|
||||
type IMessage,
|
||||
type IGetMessageParams,
|
||||
type IGetMessageResponseError,
|
||||
type IGetMessageResponseSuccess,
|
||||
type IPostCreateMessageRequest,
|
||||
type IPostCreateMessageResponseError,
|
||||
type IPostCreateMessageResponseSuccess,
|
||||
type IPatchMessageParams,
|
||||
type IPatchMessageRequest,
|
||||
type IPatchMessageResponseError,
|
||||
type IPatchMessageResponseSuccess,
|
||||
type IDeleteMessageParams,
|
||||
type IDeleteMessageResponseError,
|
||||
type IDeleteMessageResponseSuccess,
|
||||
};
|
||||
3
src/controllers/websocket/index.ts
Normal file
3
src/controllers/websocket/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./websocket.js";
|
||||
export * from "./routes.js";
|
||||
export * from "./types.js";
|
||||
12
src/controllers/websocket/routes.ts
Normal file
12
src/controllers/websocket/routes.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { type FastifyInstance } from "fastify";
|
||||
import * as controller from "./websocket.js";
|
||||
|
||||
const websocketRoutes = async (fastify: FastifyInstance) => {
|
||||
fastify.get(
|
||||
`/`,
|
||||
{ websocket: true, preHandler: controller.handleWebSockets },
|
||||
controller.getWebSockets,
|
||||
);
|
||||
};
|
||||
|
||||
export { websocketRoutes };
|
||||
7
src/controllers/websocket/types.ts
Normal file
7
src/controllers/websocket/types.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { API_ERROR } from "../errors.js";
|
||||
|
||||
interface IGetWebSocketResponseError {
|
||||
error: API_ERROR;
|
||||
}
|
||||
|
||||
export { type IGetWebSocketResponseError };
|
||||
33
src/controllers/websocket/websocket.ts
Normal file
33
src/controllers/websocket/websocket.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { type FastifyReply, type FastifyRequest } from "fastify";
|
||||
import type { IGetWebSocketResponseError } from "./types.js";
|
||||
import { API_ERROR } from "../errors.js";
|
||||
import { getUserFromCookie } from "../../services/auth/helpers.js";
|
||||
import { handleNewWebSocket } from "../../services/websocket/websocket.js";
|
||||
import type { WebSocket } from "@fastify/websocket";
|
||||
|
||||
const handleWebSockets = async (
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
const cookie = request.cookies["token"];
|
||||
const user = await getUserFromCookie(cookie);
|
||||
if (!user) {
|
||||
reply.status(403);
|
||||
return {
|
||||
error: API_ERROR.ACCESS_DENIED,
|
||||
} as IGetWebSocketResponseError;
|
||||
}
|
||||
};
|
||||
|
||||
const getWebSockets = async (socket: WebSocket, request: FastifyRequest) => {
|
||||
const cookie = request.cookies["token"];
|
||||
const user = await getUserFromCookie(cookie);
|
||||
if (!user) {
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
handleNewWebSocket(socket, user.id);
|
||||
};
|
||||
|
||||
export { handleWebSockets, getWebSockets };
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import Fastify from "fastify";
|
||||
import cors from "@fastify/cors";
|
||||
import cookie from "@fastify/cookie";
|
||||
import websocket from "@fastify/websocket";
|
||||
|
||||
import { config } from "./config.js";
|
||||
|
||||
|
|
@ -14,6 +15,8 @@ import { communityRoutes } from "./controllers/community/routes.js";
|
|||
import { channelRoutes } from "./controllers/channel/routes.js";
|
||||
import { roleRoutes } from "./controllers/role/routes.js";
|
||||
import { inviteRoutes } from "./controllers/invite/routes.js";
|
||||
import { messageRoutes } from "./controllers/message/routes.js";
|
||||
import { websocketRoutes } from "./controllers/websocket/routes.js";
|
||||
|
||||
const app = Fastify({
|
||||
logger: true,
|
||||
|
|
@ -26,6 +29,8 @@ app.register(cors, {
|
|||
|
||||
app.register(cookie, { secret: getCookieSecret() });
|
||||
|
||||
app.register(websocket);
|
||||
|
||||
app.register(testRoutes);
|
||||
app.register(authRoutes, { prefix: "/api/v1/auth" });
|
||||
app.register(userRoutes, { prefix: "/api/v1/user" });
|
||||
|
|
@ -34,6 +39,8 @@ app.register(communityRoutes, { prefix: "/api/v1/community" });
|
|||
app.register(channelRoutes, { prefix: "/api/v1/channel" });
|
||||
app.register(roleRoutes, { prefix: "/api/v1/role" });
|
||||
app.register(inviteRoutes, { prefix: "/api/v1/invite" });
|
||||
app.register(messageRoutes, { prefix: "/api/v1/message" });
|
||||
app.register(websocketRoutes, { prefix: "/ws" });
|
||||
|
||||
app.listen({ port: config.port }, (err, address) => {
|
||||
if (err) throw err;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,31 @@ const verifyPassword = async (
|
|||
return await argon2.verify(passwordHash, passwordToCheck);
|
||||
};
|
||||
|
||||
const getUserFromCookie = async (
|
||||
cookie: string | undefined,
|
||||
): Promise<User | null> => {
|
||||
if (!cookie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const session = await getDB().session.findFirst({
|
||||
where: {
|
||||
cookie: cookie,
|
||||
},
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await getUserBySessionId(session.id);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
const getUserBySessionId = async (sessionId: string): Promise<User | null> => {
|
||||
return await getDB().user.findFirst({
|
||||
where: {
|
||||
|
|
@ -116,6 +141,12 @@ const isUserOwnerOrAdmin = async (
|
|||
if (ownerCheck.role !== undefined) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
ownerCheck.message !== undefined &&
|
||||
ownerCheck.message?.ownerId !== user.id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
|
@ -214,6 +245,7 @@ export {
|
|||
verifyToken,
|
||||
hashPassword,
|
||||
verifyPassword,
|
||||
getUserFromCookie,
|
||||
getUserBySessionId,
|
||||
getUserFromAuth,
|
||||
isUserOwnerOrAdmin,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,12 @@ enum PERMISSION {
|
|||
MEMBERS_READ = "MEMBERS_READ",
|
||||
MEMBERS_KICK = "MEMBERS_KICK",
|
||||
MEMBERS_BAN = "MEMBERS_BAN",
|
||||
INVITES_READ = "INVITES_READ",
|
||||
INVITES_CREATE = "INVITES_CREATE",
|
||||
INVITES_DELETE = "INVITES_DELETE",
|
||||
MESSAGES_READ = "MESSAGES_READ",
|
||||
MESSAGES_CREATE = "MESSAGES_CREATE",
|
||||
MESSAGES_DELETE = "MESSAGES_DELETE",
|
||||
}
|
||||
|
||||
export { PERMISSION };
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type {
|
|||
Role,
|
||||
Session,
|
||||
User,
|
||||
Message,
|
||||
} from "../../generated/prisma/client.js";
|
||||
|
||||
interface AccessTokenPayload extends JwtPayload {
|
||||
|
|
@ -30,6 +31,7 @@ interface IOwnerCheck {
|
|||
invite?: Invite | null;
|
||||
channel?: Channel | null;
|
||||
role?: Role | null;
|
||||
message?: Message | null;
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { API_ERROR } from "../../controllers/errors.js";
|
||||
import type { Channel } from "../../generated/prisma/client.js";
|
||||
import type { Channel, Message } from "../../generated/prisma/client.js";
|
||||
import { getDB } from "../../store/store.js";
|
||||
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
|
||||
import { PERMISSION } from "../auth/permission.js";
|
||||
|
|
@ -136,6 +136,40 @@ const deleteChannelByIdAuth = async (
|
|||
return await deleteChannelById(id);
|
||||
};
|
||||
|
||||
const getChannelMessagesById = async (
|
||||
id: string,
|
||||
): Promise<Message[] | null> => {
|
||||
return await getDB().message.findMany({
|
||||
where: {
|
||||
channelId: id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getChannelMessagesByIdAuth = async (
|
||||
id: string,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message[] | null | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const channel = await getChannelById(id);
|
||||
const community = await getCommunityById(channel?.communityId ?? "");
|
||||
|
||||
if (
|
||||
!(await isUserAllowed(
|
||||
authUser,
|
||||
{
|
||||
community: community,
|
||||
},
|
||||
community,
|
||||
[PERMISSION.MESSAGES_READ],
|
||||
))
|
||||
) {
|
||||
return API_ERROR.ACCESS_DENIED;
|
||||
}
|
||||
|
||||
return await getChannelMessagesById(id);
|
||||
};
|
||||
|
||||
export {
|
||||
getChannelById,
|
||||
getChannelByIdAuth,
|
||||
|
|
@ -145,4 +179,6 @@ export {
|
|||
updateChannelByIdAuth,
|
||||
deleteChannelById,
|
||||
deleteChannelByIdAuth,
|
||||
getChannelMessagesById,
|
||||
getChannelMessagesByIdAuth,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ const getCommunityInvitesByIdAuth = async (
|
|||
community: community,
|
||||
},
|
||||
community,
|
||||
[PERMISSION.INVITES_CREATE],
|
||||
[PERMISSION.INVITES_READ],
|
||||
))
|
||||
) {
|
||||
return API_ERROR.ACCESS_DENIED;
|
||||
|
|
|
|||
2
src/services/message/index.ts
Normal file
2
src/services/message/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./message.js";
|
||||
export * from "./types.js";
|
||||
202
src/services/message/message.ts
Normal file
202
src/services/message/message.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import { API_ERROR } from "../../controllers/errors.js";
|
||||
import type { Message } from "../../generated/prisma/client.js";
|
||||
import { getDB } from "../../store/store.js";
|
||||
import {
|
||||
getUserFromAuth,
|
||||
isUserAllowed,
|
||||
isUserInCommunity,
|
||||
isUserOwnerOrAdmin,
|
||||
} from "../auth/helpers.js";
|
||||
import { PERMISSION } from "../auth/permission.js";
|
||||
import { getChannelById } from "../channel/channel.js";
|
||||
import { getCommunityById } from "../community/community.js";
|
||||
import { SocketMessageTypes } from "../websocket/types.js";
|
||||
import { sendMessageToUsers } from "../websocket/websocket.js";
|
||||
import type { ICreateMessage, IUpdateMessage } from "./types.js";
|
||||
|
||||
const getMessageById = async (id: string): Promise<Message | null> => {
|
||||
return await getDB().message.findUnique({
|
||||
where: { id: id },
|
||||
});
|
||||
};
|
||||
|
||||
const getMessageByIdAuth = async (
|
||||
id: string,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message | null | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const message = await getMessageById(id);
|
||||
const channel = await getChannelById(message?.channelId ?? "");
|
||||
const community = await getCommunityById(channel?.communityId ?? "");
|
||||
|
||||
if (
|
||||
!(await isUserAllowed(
|
||||
authUser,
|
||||
{
|
||||
community: community,
|
||||
},
|
||||
community,
|
||||
[PERMISSION.MESSAGES_READ],
|
||||
))
|
||||
) {
|
||||
return API_ERROR.ACCESS_DENIED;
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
const createMessage = async (
|
||||
ownerId: string,
|
||||
communityId: string,
|
||||
create: ICreateMessage,
|
||||
): Promise<Message> => {
|
||||
const message = await getDB().message.create({
|
||||
data: {
|
||||
ownerId: ownerId,
|
||||
...create,
|
||||
},
|
||||
});
|
||||
|
||||
const usersInCommunity = await getDB().user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
communities: {
|
||||
some: {
|
||||
id: communityId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const userIds = usersInCommunity.map((user) => user.id);
|
||||
|
||||
sendMessageToUsers(userIds, {
|
||||
type: SocketMessageTypes.NEW_MESSAGE,
|
||||
payload: {
|
||||
channelId: message.channelId,
|
||||
message: {
|
||||
id: message.id,
|
||||
text: message.text,
|
||||
edited: message.edited,
|
||||
ownerId: message.ownerId,
|
||||
creationDate: message.creationDate.getTime(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
const createMessageAuth = async (
|
||||
create: ICreateMessage,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const channel = await getChannelById(create.channelId);
|
||||
const community = await getCommunityById(channel?.communityId ?? "");
|
||||
|
||||
if (
|
||||
!authUser ||
|
||||
!community ||
|
||||
!(await isUserAllowed(
|
||||
authUser,
|
||||
{
|
||||
community: community,
|
||||
},
|
||||
community,
|
||||
[PERMISSION.MESSAGES_CREATE],
|
||||
))
|
||||
) {
|
||||
return API_ERROR.ACCESS_DENIED;
|
||||
}
|
||||
|
||||
return await createMessage(authUser.id, community.id, create);
|
||||
};
|
||||
|
||||
const updateMessageById = async (
|
||||
id: string,
|
||||
update: IUpdateMessage,
|
||||
): Promise<Message | null> => {
|
||||
const message = await getMessageById(id);
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newEditHistory = [...message.editHistory, message.text];
|
||||
|
||||
return await getDB().message.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: {
|
||||
...update,
|
||||
editHistory: newEditHistory,
|
||||
edited: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateMessageByIdAuth = async (
|
||||
id: string,
|
||||
update: IUpdateMessage,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message | null | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const message = await getMessageById(id);
|
||||
const channel = await getChannelById(message?.channelId ?? "");
|
||||
const community = await getCommunityById(channel?.communityId ?? "");
|
||||
|
||||
if (
|
||||
!(await isUserOwnerOrAdmin(authUser, {
|
||||
message: message,
|
||||
})) ||
|
||||
!(await isUserInCommunity(authUser, community))
|
||||
) {
|
||||
return API_ERROR.ACCESS_DENIED;
|
||||
}
|
||||
|
||||
return await updateMessageById(id, update);
|
||||
};
|
||||
|
||||
const deleteMessageById = async (id: string): Promise<Message | null> => {
|
||||
return await getDB().message.delete({
|
||||
where: { id: id },
|
||||
});
|
||||
};
|
||||
|
||||
const deleteMessageByIdAuth = async (
|
||||
id: string,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message | null | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const message = await getMessageById(id);
|
||||
const channel = await getChannelById(message?.channelId ?? "");
|
||||
const community = await getCommunityById(channel?.communityId ?? "");
|
||||
|
||||
if (
|
||||
!(await isUserAllowed(
|
||||
authUser,
|
||||
{
|
||||
community: community,
|
||||
},
|
||||
community,
|
||||
[PERMISSION.MESSAGES_DELETE],
|
||||
))
|
||||
) {
|
||||
return API_ERROR.ACCESS_DENIED;
|
||||
}
|
||||
|
||||
return await deleteMessageById(id);
|
||||
};
|
||||
|
||||
export {
|
||||
getMessageById,
|
||||
getMessageByIdAuth,
|
||||
createMessage,
|
||||
createMessageAuth,
|
||||
updateMessageById,
|
||||
updateMessageByIdAuth,
|
||||
deleteMessageById,
|
||||
deleteMessageByIdAuth,
|
||||
};
|
||||
10
src/services/message/types.ts
Normal file
10
src/services/message/types.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
interface ICreateMessage {
|
||||
text: string;
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
interface IUpdateMessage {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export { type ICreateMessage, type IUpdateMessage };
|
||||
2
src/services/websocket/index.ts
Normal file
2
src/services/websocket/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./websocket.js";
|
||||
export * from "./types.js";
|
||||
53
src/services/websocket/types.ts
Normal file
53
src/services/websocket/types.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import type { WebSocket } from "@fastify/websocket";
|
||||
import type { IGetMessagesResponseMessage } from "../../controllers/channel/types.js";
|
||||
|
||||
interface ISocketConnection {
|
||||
socket: WebSocket;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
enum SocketRequestTypes {
|
||||
PING = "PING",
|
||||
}
|
||||
|
||||
enum SocketMessageTypes {
|
||||
NEW_ANNOUNCEMENT = "NEW_ANNOUNCEMENT",
|
||||
NEW_MESSAGE = "NEW_MESSAGE",
|
||||
NEW_CHANNEL = "NEW_CHANNEL",
|
||||
}
|
||||
|
||||
type SocketRequest = {
|
||||
type: SocketRequestTypes.PING;
|
||||
};
|
||||
|
||||
type SocketMessage =
|
||||
| {
|
||||
type: SocketMessageTypes.NEW_ANNOUNCEMENT;
|
||||
payload: {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: SocketMessageTypes.NEW_MESSAGE;
|
||||
payload: {
|
||||
channelId: string;
|
||||
message: IGetMessagesResponseMessage;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: SocketMessageTypes.NEW_CHANNEL;
|
||||
payload: {
|
||||
id: string;
|
||||
communityId: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
type ISocketConnection,
|
||||
SocketRequestTypes,
|
||||
SocketMessageTypes,
|
||||
type SocketRequest,
|
||||
type SocketMessage,
|
||||
};
|
||||
75
src/services/websocket/websocket.ts
Normal file
75
src/services/websocket/websocket.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import type { CloseEvent, MessageEvent } from "ws";
|
||||
import type { WebSocket } from "@fastify/websocket";
|
||||
import type {
|
||||
ISocketConnection,
|
||||
SocketRequest,
|
||||
SocketMessage,
|
||||
} from "./types.js";
|
||||
|
||||
const userConnections = new Map<string, Set<ISocketConnection>>();
|
||||
|
||||
const handleNewWebSocket = (socket: WebSocket, userId: string) => {
|
||||
const connection = {
|
||||
socket: socket,
|
||||
userId: userId,
|
||||
};
|
||||
|
||||
if (!userConnections.has(userId)) {
|
||||
userConnections.set(userId, new Set());
|
||||
}
|
||||
userConnections.get(userId)?.add(connection);
|
||||
|
||||
onCloseWsHandler(connection);
|
||||
onMessageWsHandler(connection);
|
||||
};
|
||||
|
||||
const handleRequest = (data: string): SocketRequest | null => {
|
||||
const request = JSON.parse(data) as SocketRequest;
|
||||
|
||||
if (!request.type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
const onCloseWsHandler = (connection: ISocketConnection) => {
|
||||
connection.socket.onclose = (event: CloseEvent) => {
|
||||
const connections = userConnections.get(connection.userId);
|
||||
|
||||
connections?.delete(connection);
|
||||
if (connections?.size === 0) {
|
||||
userConnections.delete(connection.userId);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const onMessageWsHandler = (connection: ISocketConnection) => {
|
||||
connection.socket.onmessage = (event: MessageEvent) => {
|
||||
const request = handleRequest(event.data.toString());
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const sendMessageToUser = (userId: string, message: SocketMessage) => {
|
||||
const connections = userConnections.get(userId);
|
||||
|
||||
connections?.forEach((connection) => {
|
||||
connection.socket.send(JSON.stringify(message));
|
||||
});
|
||||
};
|
||||
|
||||
const sendMessageToUsers = (userIds: string[], message: SocketMessage) => {
|
||||
userIds?.forEach((userId) => {
|
||||
sendMessageToUser(userId, message);
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
userConnections,
|
||||
handleNewWebSocket,
|
||||
sendMessageToUser,
|
||||
sendMessageToUsers,
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue