From a378cd13e14b51726e5769b7a7537930c31f8ba9 Mon Sep 17 00:00:00 2001 From: Romaric Mourgues Date: Tue, 16 May 2023 14:36:39 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B9=20Propose=20to=20remove=20channels?= =?UTF-8?q?,=20notifications,=20knowledge-graph=20and=20phpnode=20(#55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Propose to remove channels and notifications * Propose to remove knowledge graph too * Put back tags test * FIxing tests * FIx query builder test --- .../config/custom-environment-variables.json | 12 - tdrive/backend/node/config/default.json | 11 - tdrive/backend/node/config/test.json | 3 - tdrive/backend/node/src/@types/fastify.ts | 1 - .../cli/cmds/export_cmds/export_company.ts | 87 -- .../src/cli/cmds/search_cmds/index_all.ts | 2 - .../platform/services/email-pusher/types.ts | 2 - .../services/knowledge-graph/api-client.ts | 151 --- .../services/knowledge-graph/index.ts | 144 --- .../services/knowledge-graph/provider.ts | 13 - .../services/knowledge-graph/types.ts | 109 -- .../core/platform/services/phpnode/index.ts | 189 ---- .../platform/services/phpnode/provider.ts | 11 - .../core/platform/services/tracker/index.ts | 40 - .../channels/entities/channel-activity.ts | 59 -- .../channels/entities/channel-counters.ts | 35 - .../entities/channel-member-read-cursors.ts | 34 - .../channels/entities/channel-member.ts | 138 --- .../entities/channel-pending-emails.ts | 46 - .../channels/entities/channel.search.ts | 24 - .../src/services/channels/entities/channel.ts | 108 -- .../channels/entities/default-channel.ts | 34 - .../channels/entities/direct-channel.ts | 47 - .../src/services/channels/entities/index.ts | 16 - .../src/services/channels/entities/member.ts | 6 - .../src/services/channels/entities/tab.ts | 55 - .../node/src/services/channels/index.ts | 23 - .../services/channel/activities/service.ts | 223 ---- .../services/channel/activities/types.ts | 29 - .../services/channel/default/listener.ts | 113 -- .../services/channel/default/service.ts | 260 ----- .../channel/pending-emails/service.ts | 180 ---- .../channels/services/channel/realtime.ts | 52 - .../channels/services/channel/service.ts | 987 ------------------ .../channels/services/channel/types.ts | 86 -- .../channels/services/member/realtime.ts | 41 - .../channels/services/member/service.ts | 911 ---------------- .../channels/services/pubsub/index.ts | 22 - .../services/pubsub/new-channel-activity.ts | 91 -- ...nding-emails-in-workspace-join-channels.ts | 58 - ...user-in-workspace-join-default-channels.ts | 59 -- .../channels/services/pubsub/new-workspace.ts | 57 - .../src/services/channels/services/tab.ts | 136 --- .../node/src/services/channels/types.ts | 54 - .../node/src/services/channels/utils.ts | 5 - .../channels/web/controllers/channel.ts | 623 ----------- .../channels/web/controllers/index.ts | 3 - .../channels/web/controllers/member.ts | 319 ------ .../services/channels/web/controllers/tab.ts | 204 ---- .../node/src/services/channels/web/index.ts | 12 - .../src/services/channels/web/middleware.ts | 12 - .../node/src/services/channels/web/routes.ts | 270 ----- .../node/src/services/channels/web/schemas.ts | 301 ------ .../node/src/services/channels/web/types.ts | 117 --- .../node/src/services/global-resolver.ts | 47 - .../src/services/knowledge-graph-web/index.ts | 25 - .../services/knowledge-graph-web/web/index.ts | 12 - .../knowledge-graph-web/web/routes.ts | 41 - ...channel-member-notification-preferences.ts | 50 - .../entities/channel-thread-users.ts | 50 - .../services/notifications/entities/index.ts | 20 - .../entities/user-notification-badges.ts | 78 -- .../entities/user-notification-digest.ts | 43 - .../node/src/services/notifications/index.ts | 22 - .../services/notifications/notifiers/index.ts | 5 - .../notifications/notifiers/mobile-push.ts | 32 - .../services/notifications/services/bages.ts | 261 ----- .../services/channel-preferences.ts | 157 --- .../services/channel-thread-users.ts | 68 -- .../services/notifications/services/digest.ts | 154 --- .../notifications/services/engine/index.ts | 34 - .../processors/channel-member-created.ts | 79 -- .../processors/channel-member-deleted.ts | 101 -- .../processors/channel-member-updated.ts | 55 - .../engine/processors/mark-channel-as-read.ts | 70 -- .../processors/mark-channel-as-unread.ts | 68 -- .../processors/mobile-push-notifications.ts | 48 - .../engine/processors/push-to-users.ts | 213 ---- .../processors/reaction-notification.ts | 127 --- .../notifications/services/mobile-push.ts | 59 -- .../notifications/services/preferences.ts | 147 --- .../notifications/services/realtime.ts | 12 - .../node/src/services/notifications/types.ts | 97 -- .../notifications/web/controllers/index.ts | 2 - .../web/controllers/notification.ts | 83 -- .../web/controllers/preferences.ts | 78 -- .../src/services/notifications/web/index.ts | 12 - .../services/notifications/web/middleware.ts | 0 .../src/services/notifications/web/routes.ts | 37 - .../src/services/notifications/web/schemas.ts | 43 - .../src/services/notifications/web/types.ts | 0 .../src/services/user/services/companies.ts | 13 - .../services/user/services/users/service.ts | 22 +- .../node/src/services/user/web/controller.ts | 33 +- .../services/workspaces/services/workspace.ts | 13 - .../controllers/workspace-invite-tokens.ts | 17 - .../web/controllers/workspace-users.ts | 23 - tdrive/backend/node/src/utils/types.ts | 16 +- .../backend/node/test/e2e/tags/tags.spec.ts | 306 ++++++ .../cassandra/query-builder.test.ts | 47 +- .../backend-node/production.json | 3 - .../backend-node/production.json.template | 3 - 102 files changed, 327 insertions(+), 9224 deletions(-) delete mode 100644 tdrive/backend/node/src/core/platform/services/knowledge-graph/api-client.ts delete mode 100644 tdrive/backend/node/src/core/platform/services/knowledge-graph/index.ts delete mode 100644 tdrive/backend/node/src/core/platform/services/knowledge-graph/provider.ts delete mode 100644 tdrive/backend/node/src/core/platform/services/knowledge-graph/types.ts delete mode 100644 tdrive/backend/node/src/core/platform/services/phpnode/index.ts delete mode 100644 tdrive/backend/node/src/core/platform/services/phpnode/provider.ts delete mode 100644 tdrive/backend/node/src/services/channels/entities/channel-activity.ts delete mode 100644 tdrive/backend/node/src/services/channels/entities/channel-counters.ts delete mode 100644 tdrive/backend/node/src/services/channels/entities/channel-member-read-cursors.ts delete mode 100644 tdrive/backend/node/src/services/channels/entities/channel-member.ts delete mode 100644 tdrive/backend/node/src/services/channels/entities/channel-pending-emails.ts delete mode 100644 tdrive/backend/node/src/services/channels/entities/channel.search.ts delete mode 100644 tdrive/backend/node/src/services/channels/entities/channel.ts delete mode 100644 tdrive/backend/node/src/services/channels/entities/default-channel.ts delete mode 100644 tdrive/backend/node/src/services/channels/entities/direct-channel.ts delete mode 100644 tdrive/backend/node/src/services/channels/entities/index.ts delete mode 100644 tdrive/backend/node/src/services/channels/entities/member.ts delete mode 100644 tdrive/backend/node/src/services/channels/entities/tab.ts delete mode 100644 tdrive/backend/node/src/services/channels/index.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/channel/activities/service.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/channel/activities/types.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/channel/default/listener.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/channel/default/service.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/channel/pending-emails/service.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/channel/realtime.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/channel/service.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/channel/types.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/member/realtime.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/member/service.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/pubsub/index.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/pubsub/new-channel-activity.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/pubsub/new-pending-emails-in-workspace-join-channels.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/pubsub/new-user-in-workspace-join-default-channels.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/pubsub/new-workspace.ts delete mode 100644 tdrive/backend/node/src/services/channels/services/tab.ts delete mode 100644 tdrive/backend/node/src/services/channels/types.ts delete mode 100644 tdrive/backend/node/src/services/channels/utils.ts delete mode 100644 tdrive/backend/node/src/services/channels/web/controllers/channel.ts delete mode 100644 tdrive/backend/node/src/services/channels/web/controllers/index.ts delete mode 100644 tdrive/backend/node/src/services/channels/web/controllers/member.ts delete mode 100644 tdrive/backend/node/src/services/channels/web/controllers/tab.ts delete mode 100644 tdrive/backend/node/src/services/channels/web/index.ts delete mode 100644 tdrive/backend/node/src/services/channels/web/middleware.ts delete mode 100644 tdrive/backend/node/src/services/channels/web/routes.ts delete mode 100644 tdrive/backend/node/src/services/channels/web/schemas.ts delete mode 100644 tdrive/backend/node/src/services/channels/web/types.ts delete mode 100644 tdrive/backend/node/src/services/knowledge-graph-web/index.ts delete mode 100644 tdrive/backend/node/src/services/knowledge-graph-web/web/index.ts delete mode 100644 tdrive/backend/node/src/services/knowledge-graph-web/web/routes.ts delete mode 100644 tdrive/backend/node/src/services/notifications/entities/channel-member-notification-preferences.ts delete mode 100644 tdrive/backend/node/src/services/notifications/entities/channel-thread-users.ts delete mode 100644 tdrive/backend/node/src/services/notifications/entities/index.ts delete mode 100644 tdrive/backend/node/src/services/notifications/entities/user-notification-badges.ts delete mode 100644 tdrive/backend/node/src/services/notifications/entities/user-notification-digest.ts delete mode 100644 tdrive/backend/node/src/services/notifications/index.ts delete mode 100644 tdrive/backend/node/src/services/notifications/notifiers/index.ts delete mode 100644 tdrive/backend/node/src/services/notifications/notifiers/mobile-push.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/bages.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/channel-preferences.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/channel-thread-users.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/digest.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/engine/index.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/engine/processors/channel-member-created.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/engine/processors/channel-member-deleted.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/engine/processors/channel-member-updated.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/engine/processors/mark-channel-as-read.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/engine/processors/mark-channel-as-unread.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/engine/processors/mobile-push-notifications.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/engine/processors/push-to-users.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/engine/processors/reaction-notification.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/mobile-push.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/preferences.ts delete mode 100644 tdrive/backend/node/src/services/notifications/services/realtime.ts delete mode 100644 tdrive/backend/node/src/services/notifications/types.ts delete mode 100644 tdrive/backend/node/src/services/notifications/web/controllers/index.ts delete mode 100644 tdrive/backend/node/src/services/notifications/web/controllers/notification.ts delete mode 100644 tdrive/backend/node/src/services/notifications/web/controllers/preferences.ts delete mode 100644 tdrive/backend/node/src/services/notifications/web/index.ts delete mode 100644 tdrive/backend/node/src/services/notifications/web/middleware.ts delete mode 100644 tdrive/backend/node/src/services/notifications/web/routes.ts delete mode 100644 tdrive/backend/node/src/services/notifications/web/schemas.ts delete mode 100644 tdrive/backend/node/src/services/notifications/web/types.ts create mode 100644 tdrive/backend/node/test/e2e/tags/tags.spec.ts diff --git a/tdrive/backend/node/config/custom-environment-variables.json b/tdrive/backend/node/config/custom-environment-variables.json index 5ab9dea6..81339917 100644 --- a/tdrive/backend/node/config/custom-environment-variables.json +++ b/tdrive/backend/node/config/custom-environment-variables.json @@ -24,9 +24,6 @@ "root": "STATIC_ROOT" } }, - "phpnode": { - "secret": "PHP_NODE_API_SECRET" - }, "websocket": { "auth": { "jwt": { @@ -44,15 +41,6 @@ "secret": "AUTH_JWT_SECRET" } }, - "notifications": { - "push": { - "type": "PUSH_DRIVER", - "fcm": { - "endpoint": "PUSH_FCM_ENDPOINT", - "key": "PUSH_FCM_KEY" - } - } - }, "database": { "secret": "DB_SECRET", "type": "DB_DRIVER", diff --git a/tdrive/backend/node/config/default.json b/tdrive/backend/node/config/default.json index eee596d4..d6446a9f 100644 --- a/tdrive/backend/node/config/default.json +++ b/tdrive/backend/node/config/default.json @@ -127,12 +127,6 @@ "path": "/storage/" } }, - "knowledge-graph": { - "endpoint": "http://host-gateway:8888", - "callback_token": "secret", - "use": false, - "forwarded_companies": [] - }, "email-pusher": { "endpoint": "https://api.smtp2go.com/v3/email/send", "api_key": "secret", @@ -153,12 +147,9 @@ "search", "message-queue", "realtime", - "phpnode", "tracker", "general", "user", - "channels", - "notifications", "files", "workspaces", "console", @@ -166,8 +157,6 @@ "statistics", "cron", "online", - "knowledge-graph", - "knowledge-graph-web", "email-pusher", "documents", "applications", diff --git a/tdrive/backend/node/config/test.json b/tdrive/backend/node/config/test.json index 682f49df..5918383c 100644 --- a/tdrive/backend/node/config/test.json +++ b/tdrive/backend/node/config/test.json @@ -19,8 +19,5 @@ "amqp": { "urls": ["amqp://guest:guest@rabbitmq:5672"] } - }, - "phpnode": { - "php_endpoint": false } } diff --git a/tdrive/backend/node/src/@types/fastify.ts b/tdrive/backend/node/src/@types/fastify.ts index f30ed5f0..df220be9 100644 --- a/tdrive/backend/node/src/@types/fastify.ts +++ b/tdrive/backend/node/src/@types/fastify.ts @@ -9,7 +9,6 @@ export interface authenticateDecorator { declare module "fastify" { interface FastifyInstance { - phpnodeAuthenticate(): void; authenticate(): void; authenticateOptional(): void; io: SocketIO.Server; diff --git a/tdrive/backend/node/src/cli/cmds/export_cmds/export_company.ts b/tdrive/backend/node/src/cli/cmds/export_cmds/export_company.ts index 6615a102..c10298f1 100644 --- a/tdrive/backend/node/src/cli/cmds/export_cmds/export_company.ts +++ b/tdrive/backend/node/src/cli/cmds/export_cmds/export_company.ts @@ -3,8 +3,6 @@ import tdrive from "../../../tdrive"; import { mkdirSync, writeFileSync } from "fs"; import { Pagination } from "../../../core/platform/framework/api/crud-service"; import WorkspaceUser from "../../../services/workspaces/entities/workspace_user"; -import { ChannelVisibility } from "../../../services/channels/types"; -import { Channel, ChannelMember } from "../../../services/channels/entities"; import { formatCompany } from "../../../services/user/utils"; import gr from "../../../services/global-resolver"; import { formatUser } from "../../../utils/users"; @@ -105,91 +103,6 @@ const command: yargs.CommandModule = { } writeFileSync(`${output}/users.json`, JSON.stringify(users)); - //Channels - console.log("- Create channels json file"); - const directChannels: Channel[] = []; - let allPublicChannels: Channel[] = []; - - let pagination = new Pagination(); - do { - const page = await gr.services.channels.channels.getDirectChannelsInCompany( - pagination, - company.id, - undefined, - ); - for (const channel of page.getEntities()) { - const channelDetail = await gr.services.channels.channels.get( - { - company_id: channel.company_id, - workspace_id: "direct", - id: channel.id, - }, - undefined, - ); - directChannels.push(channelDetail); - } - pagination = page.nextPage as Pagination; - } while (pagination.page_token); - - for (const workspace of workspaces) { - let pagination = new Pagination(); - - let publicChannels: Channel[] = []; - pagination = new Pagination(); - do { - const page = await gr.services.channels.channels.list( - pagination, - {}, - { - user: { id: "", server_request: true }, - workspace: { workspace_id: workspace.id, company_id: company.id }, - }, - ); - const chans = page.getEntities().filter(c => c.visibility == ChannelVisibility.PUBLIC); - allPublicChannels = [...allPublicChannels, ...chans]; - publicChannels = [...publicChannels, ...chans]; - pagination = page.nextPage as Pagination; - } while (pagination.page_token); - - mkdirSync(`${output}/workspaces/${workspace.id}`, { recursive: true }); - writeFileSync( - `${output}/workspaces/${workspace.id}/channels.json`, - JSON.stringify(publicChannels), - ); - } - writeFileSync(`${output}/direct_channels.json`, JSON.stringify(directChannels)); - - //Channels users - console.log("- Create channels users json file"); - for (const channel of [...allPublicChannels /*, ...directChannels*/]) { - let members: ChannelMember[] = []; - let pagination = new Pagination(); - do { - const page = await gr.services.channels.members.list( - pagination, - {}, - { - user: { id: "", server_request: true }, - channel: { - company_id: channel.company_id, - workspace_id: channel.workspace_id, - id: channel.id, - }, - }, - ); - members = [...members, ...page.getEntities()] as ChannelMember[]; - pagination = page.nextPage as Pagination; - } while (pagination.page_token); - - mkdirSync(`${output}/workspaces/${channel.workspace_id}/channels/${channel.id}`, { - recursive: true, - }); - writeFileSync( - `${output}/workspaces/${channel.workspace_id}/channels/${channel.id}/members.json`, - JSON.stringify(members), - ); - } - await platform.stop(); }, }; diff --git a/tdrive/backend/node/src/cli/cmds/search_cmds/index_all.ts b/tdrive/backend/node/src/cli/cmds/search_cmds/index_all.ts index 4de1a6b2..04c5f1a4 100644 --- a/tdrive/backend/node/src/cli/cmds/search_cmds/index_all.ts +++ b/tdrive/backend/node/src/cli/cmds/search_cmds/index_all.ts @@ -6,7 +6,6 @@ import { DatabaseServiceAPI } from "../../../core/platform/services/database/api import { Pagination } from "../../../core/platform/framework/api/crud-service"; import User, { TYPE as UserTYPE } from "../../../services/user/entities/user"; -import { Channel } from "../../../services/channels/entities"; import Repository from "../../../core/platform/services/database/services/orm/repository/repository"; import { SearchServiceAPI } from "../../../core/platform/services/search/api"; import CompanyUser, { TYPE as CompanyUserTYPE } from "../../../services/user/entities/company_user"; @@ -29,7 +28,6 @@ class SearchIndexAll { public async run(options: Options = {}): Promise { const repositories: Map> = new Map(); repositories.set("users", await this.database.getRepository(UserTYPE, User)); - repositories.set("channels", await this.database.getRepository("channels", Channel)); const repository = repositories.get(options.repository); if (!repository) { diff --git a/tdrive/backend/node/src/core/platform/services/email-pusher/types.ts b/tdrive/backend/node/src/core/platform/services/email-pusher/types.ts index 6dff9e45..36073604 100644 --- a/tdrive/backend/node/src/core/platform/services/email-pusher/types.ts +++ b/tdrive/backend/node/src/core/platform/services/email-pusher/types.ts @@ -1,4 +1,3 @@ -import { Channel } from "../../../../services/channels/entities/channel"; import Company from "../../../../services/user/entities/company"; import Workspace from "../../../../services/workspaces/entities/workspace"; import User from "../../../../services/user/entities/user"; @@ -7,7 +6,6 @@ export type EmailBuilderDataPayload = { user: User; company: Company; notifications: { - channel: Channel; workspace: Workspace; }[]; }; diff --git a/tdrive/backend/node/src/core/platform/services/knowledge-graph/api-client.ts b/tdrive/backend/node/src/core/platform/services/knowledge-graph/api-client.ts deleted file mode 100644 index 5734293e..00000000 --- a/tdrive/backend/node/src/core/platform/services/knowledge-graph/api-client.ts +++ /dev/null @@ -1,151 +0,0 @@ -import axios, { AxiosInstance } from "axios"; - -import { KnowledgeGraphCreateBodyRequest, KnowledgeGraphCreateMessageObjectData } from "./types"; - -import { md5 } from "../../../../core/crypto"; -import { Channel } from "../../../../services/channels/entities"; -import gr from "../../../../services/global-resolver"; -import Company from "../../../../services/user/entities/company"; -import User from "../../../../services/user/entities/user"; -import Workspace from "../../../../services/workspaces/entities/workspace"; -import { getLogger, TdriveLogger } from "../../framework"; - -export default class KnowledgeGraphAPIClient { - protected readonly version = "1.0.0"; - protected readonly axiosInstance: AxiosInstance = axios.create(); - readonly apiUrl: string; - readonly logger: TdriveLogger = getLogger("knowledge-graph-api-client"); - - constructor(apiUrl: string) { - this.apiUrl = apiUrl; - } - - private async getUserKGId(id: string, provider_id?: string) { - provider_id = provider_id || (await gr.services.users.get({ id }))?.identity_provider_id; - return provider_id; - } - - private async getUserKGMailId(id: string, email?: string) { - email = email || (await gr.services.users.get({ id }))?.email_canonical; - return md5(email.trim().toLocaleLowerCase()); - } - - private async getCompanyKGId(id: string, identity_provider_id?: string) { - identity_provider_id = - identity_provider_id || - (await gr.services.companies.getCompany({ id }))?.identity_provider_id || - id; - return identity_provider_id; - } - - public async onCompanyCreated(company: Partial): Promise { - this.send({ - records: [ - { - key: "null", - value: { - id: "Company", - properties: { - _kg_company_id: await this.getCompanyKGId(company.id, company.identity_provider_id), - company_id: company.id, - company_name: company.displayName || company.name, - }, - }, - }, - ], - }); - } - - public async onWorkspaceCreated(workspace: Partial): Promise { - const response = await this.send({ - records: [ - { - key: "null", - value: { - id: "Workspace", - properties: { - _kg_company_id: await this.getCompanyKGId(workspace.company_id), - company_id: workspace.company_id, - workspace_name: workspace.name, - workspace_id: workspace.id, - }, - }, - }, - ], - }); - - if (response.statusText === "OK") { - this.logger.info("onWorkspaceCreated %o", response.config.data); - } - } - - public async onUserCreated(companyId: string, user: Partial): Promise { - const response = await this.send({ - records: [ - { - key: "null", - value: { - id: "User", - properties: { - _kg_user_id: await this.getUserKGId(user.id, user.identity_provider_id), - _kg_email_id: await this.getUserKGMailId(user.id, user.email_canonical), - _kg_company_all_id: await Promise.all( - user.cache.companies.map(async c => await this.getCompanyKGId(c)), - ), - user_id: user.id, - email: user.email_canonical, - username: user.username_canonical, - user_last_activity: user.last_activity, - first_name: user.first_name, - user_created_at: user.creation_date, - last_name: user.last_name, - company_id: companyId, - }, - }, - }, - ], - }); - - if (response.statusText === "OK") { - this.logger.info("onUserCreated %o", response.config.data); - } - } - - public async onChannelCreated(channel: Partial): Promise { - const response = await this.send({ - records: [ - { - key: "null", - value: { - id: "Channel", - properties: { - _kg_user_id: await this.getUserKGId(channel.owner), - _kg_email_id: await this.getUserKGMailId(channel.owner), - _kg_company_id: await this.getCompanyKGId(channel.company_id), - channel_id: channel.id, - channel_name: channel.name, - channel_owner: channel.owner, - workspace_id: channel.workspace_id, - company_id: channel.company_id, - }, - }, - }, - ], - }); - - if (response.statusText === "OK") { - this.logger.info("onChannelCreated %o", response.config.data); - } - } - - private async send(data: any) { - return await this.axiosInstance.post< - KnowledgeGraphCreateBodyRequest - >(`${this.apiUrl}/topics/tdrive`, data, { - headers: { - "Content-Type": "application/vnd.kafka.json.v2+json", - Accept: "application/vnd.kafka.v2+json", - }, - }); - } -} diff --git a/tdrive/backend/node/src/core/platform/services/knowledge-graph/index.ts b/tdrive/backend/node/src/core/platform/services/knowledge-graph/index.ts deleted file mode 100644 index 579bbcf2..00000000 --- a/tdrive/backend/node/src/core/platform/services/knowledge-graph/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Configuration, Consumes, getLogger, TdriveLogger, TdriveService } from "../../framework"; -import { localEventBus } from "../../framework/event-bus"; -import KnowledgeGraphAPI from "./provider"; -import Workspace from "../../../../services/workspaces/entities/workspace"; -import Company from "../../../../services/user/entities/company"; -import User from "../../../../services/user/entities/user"; -import { Channel } from "../../../../services/channels/entities"; -import { - KnowledgeGraphGenericEventPayload, - KnowledgeGraphEvents, - KnowledgeGraphCallbackEvent, -} from "./types"; -import KnowledgeGraphAPIClient from "./api-client"; -import gr from "../../../../services/global-resolver"; - -@Consumes([]) -export default class KnowledgeGraphService - extends TdriveService - implements KnowledgeGraphAPI -{ - readonly name = "knowledge-graph"; - readonly version = "1.0.0"; - protected kgAPIClient: KnowledgeGraphAPIClient = this.getKnowledgeGraphApiClient(); - logger: TdriveLogger = getLogger("knowledge-graph-service"); - - async doInit(): Promise { - const use = this.getConfigurationEntry("use"); - - if (!use) { - this.logger.warn("Knowledge graph is not used"); - - return this; - } - - localEventBus.subscribe>( - KnowledgeGraphEvents.COMPANY_UPSERT, - this.onCompanyCreated.bind(this), - ); - - localEventBus.subscribe>( - KnowledgeGraphEvents.WORKSPACE_UPSERT, - this.onWorkspaceCreated.bind(this), - ); - - localEventBus.subscribe>( - KnowledgeGraphEvents.CHANNEL_UPSERT, - this.onChannelCreated.bind(this), - ); - - localEventBus.subscribe>( - KnowledgeGraphEvents.USER_UPSERT, - this.onUserCreated.bind(this), - ); - - return this; - } - - /** When the KG service send us new events */ - async onCallbackEvent(token: string, data: KnowledgeGraphCallbackEvent): Promise { - if (token === this.getConfigurationEntry("callback_token")) { - this.logger.info("Unimplemented: KnowledgeGraph - Callback event", data); - } else { - throw new Error("Invalid token"); - } - } - - async onCompanyCreated(data: KnowledgeGraphGenericEventPayload): Promise { - this.logger.info(`${KnowledgeGraphEvents.COMPANY_UPSERT} %o`, data); - - if (this.kgAPIClient && (await this.shouldForwardEvent([data.resource.id]))) { - this.kgAPIClient.onCompanyCreated(data.resource); - } - } - - async onWorkspaceCreated(data: KnowledgeGraphGenericEventPayload): Promise { - this.logger.info(`${KnowledgeGraphEvents.WORKSPACE_UPSERT} %o`, data); - - if (this.kgAPIClient && (await this.shouldForwardEvent([data.resource.company_id]))) { - this.kgAPIClient.onWorkspaceCreated(data.resource); - } - } - - async onChannelCreated(data: KnowledgeGraphGenericEventPayload): Promise { - this.logger.info(`${KnowledgeGraphEvents.CHANNEL_UPSERT} %o`, data); - - if (this.kgAPIClient && (await this.shouldForwardEvent([data.resource.company_id]))) { - this.kgAPIClient.onChannelCreated(data.resource); - } - } - - async onUserCreated(data: KnowledgeGraphGenericEventPayload): Promise { - this.logger.info(`${KnowledgeGraphEvents.USER_UPSERT} %o`, data); - - if ( - this.kgAPIClient && - (await this.shouldForwardEvent(data.resource.cache?.companies || [], data.resource.id)) - ) { - for (const companyId of data.resource.cache?.companies || []) { - this.kgAPIClient.onUserCreated(companyId, data.resource); - } - } - } - - private getConfigurationEntry(key: string): T { - const configuration = new Configuration("knowledge-graph"); - return configuration.get(key); - } - - private getKnowledgeGraphApiClient(): KnowledgeGraphAPIClient { - const endpoint = this.getConfigurationEntry("endpoint"); - - if (endpoint && endpoint.length) { - this.kgAPIClient = new KnowledgeGraphAPIClient(endpoint); - } else { - this.logger.info("KnowledgeGraph - No endpoint defined in default.json"); - } - - return this.kgAPIClient; - } - - async shouldForwardEvent( - companyIds: string[] | null, - userId?: string, - ): Promise { - const user = userId ? await gr.services.users.get({ id: userId }) : null; - const forwardedCompanies = this.getConfigurationEntry("forwarded_companies"); - const isCompanyForwarded = !!(companyIds || []).find(v => forwardedCompanies.includes(v)); - if (user?.preferences && !user.preferences.knowledge_graph) - user.preferences.knowledge_graph = "metadata"; - return (!userId || (user && user.preferences?.knowledge_graph !== "nothing")) && - (!companyIds || - companyIds.length === 0 || - isCompanyForwarded || - forwardedCompanies.length === 0) - ? user - ? (user.preferences.knowledge_graph as "all" | "metadata") - : "all" - : false; - } - - api(): KnowledgeGraphAPI { - return this; - } -} diff --git a/tdrive/backend/node/src/core/platform/services/knowledge-graph/provider.ts b/tdrive/backend/node/src/core/platform/services/knowledge-graph/provider.ts deleted file mode 100644 index 612fd0f9..00000000 --- a/tdrive/backend/node/src/core/platform/services/knowledge-graph/provider.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Company from "../../../../services/user/entities/company"; -import { Channel } from "../../../../services/channels/entities"; -import { TdriveServiceProvider } from "../../framework"; -import { KnowledgeGraphGenericEventPayload } from "./types"; -import Workspace from "../../../../services/workspaces/entities/workspace"; -import User from "../../../../services/user/entities/user"; - -export default interface KnowledgeGraphAPI extends TdriveServiceProvider { - onCompanyCreated(data: KnowledgeGraphGenericEventPayload): void; - onWorkspaceCreated(data: KnowledgeGraphGenericEventPayload): void; - onChannelCreated(data: KnowledgeGraphGenericEventPayload): void; - onUserCreated(data: KnowledgeGraphGenericEventPayload): void; -} diff --git a/tdrive/backend/node/src/core/platform/services/knowledge-graph/types.ts b/tdrive/backend/node/src/core/platform/services/knowledge-graph/types.ts deleted file mode 100644 index a6d38a4d..00000000 --- a/tdrive/backend/node/src/core/platform/services/knowledge-graph/types.ts +++ /dev/null @@ -1,109 +0,0 @@ -export type KnowledgeGraphCreateCompanyObjectData = { - key: string; - value: { - id: string; - properties: { - company_id: string; - company_name: string; - }; - }; -}; - -export type KnowledgeGraphCreateWorkspaceObjectData = { - key: string; - value: { - id: string; - properties: { - company_id: string; - workspace_name: string; - workspace_id: string; - }; - }; -}; - -export type KnowledgeGraphCreateUserObjectData = { - key: string; - value: { - id: string; - properties: { - user_id: string; - email: string; - username: string; - user_last_activity: string; - first_name: string; - user_created_at: string; - last_name: string; - company_id: string; - }; - }; -}; - -export type KnowledgeGraphCreateChannelObjectData = { - key: string; - value: { - id: string; - properties: { - channel_id: string; - channel_name: string; - channel_owner: string; - workspace_id: string; - }; - }; -}; - -export type KnowledgeGraphCreateMessageObjectData = { - key: string; - value: { - id: string; - properties: { - message_thread_id: string; - message_created_at: string; - message_content: string; - type_message: string; - message_updated_at: string; - user_id: string; - channel_id: string; - }; - }; -}; - -export type KnowledgeGraphCreateBodyRequest = { - records: T; -}; - -export type KnowledgeGraphRelationLinkObject = { - relation: "mention" | "sender" | "parent" | "children" | "owner"; - type: "user" | "channel" | "workspace" | "company" | "message"; - id: string; -}; - -export type KnowledgeGraphGenericEventPayload = { - id: string; - resource: Partial; - links: KnowledgeGraphRelationLinkObject[]; -}; - -export enum KnowledgeGraphEvents { - COMPANY_UPSERT = "kg:company:upsert", - WORKSPACE_UPSERT = "kg:workspace:upsert", - CHANNEL_UPSERT = "kg:channel:upsert", - MESSAGE_UPSERT = "kg:message:upsert", - USER_UPSERT = "kg:user:upsert", -} - -export type KnowledgeGraphCallbackEvent = { - recipients: { - type: "user"; - id: string; // KG user id which is a md5 of the email - }[]; - event: { - type: "user_tags"; //More events will be added later - data: { - //For user_tags event only - tags?: { - value: string; - weight: number; - }[]; - }; - }; -}; diff --git a/tdrive/backend/node/src/core/platform/services/phpnode/index.ts b/tdrive/backend/node/src/core/platform/services/phpnode/index.ts deleted file mode 100644 index 36eda3e6..00000000 --- a/tdrive/backend/node/src/core/platform/services/phpnode/index.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { FastifyRequest, RouteHandlerMethod } from "fastify"; -import { FastifyInstance } from "fastify/types/instance"; -import { IncomingMessage, Server, ServerResponse } from "http"; -import { - ChannelCrudController, - ChannelMemberCrudController, -} from "../../../../services/channels/web/controllers"; -import { - ChannelMemberParameters, - ChannelParameters, - CreateChannelBody, -} from "../../../../services/channels/web/types"; -import { Consumes, TdriveService } from "../../framework"; -import WebServerAPI from "../webserver/provider"; -import WebSocketAPI from "../websocket/provider"; -import PhpNodeAPI from "./provider"; -import { RealtimeServiceAPI } from "../realtime/api"; -import gr from "../../../../services/global-resolver"; - -@Consumes(["webserver", "websocket", "user", "channels"]) -export default class PhpNodeService extends TdriveService implements PhpNodeAPI { - name = "phpnode"; - version = "1"; - private server: FastifyInstance; - private ws: WebSocketAPI; - private realtime: RealtimeServiceAPI; - - api(): PhpNodeAPI { - return this; - } - - async accessControl( - request: FastifyRequest, - server: FastifyInstance, - ) { - const token = (request.headers.authorization || "").trim().split("Token ").pop(); - const secret = this.configuration.get("secret", ""); - let authorized = false; - if (secret && token === secret) { - authorized = true; - } - - if (!authorized) { - throw server.httpErrors.badRequest("Internal Access Refused"); - } - } - - register(paremeters: { - method: "DELETE" | "GET" | "POST" | "PUT"; - url: string; - handler: RouteHandlerMethod; - }) { - this.server.register((instance, _opts, next) => { - instance.register( - (internalServer, _, next) => { - internalServer.route({ - method: paremeters.method, - url: paremeters.url, - preValidation: [request => this.accessControl(request, internalServer)], - handler: paremeters.handler, - }); - next(); - }, - { prefix: "/private" }, - ); - next(); - }); - } - - async doStart(): Promise { - return this; - } - - async doInit(): Promise { - this.server = this.context.getProvider("webserver").getServer(); - this.ws = this.context.getProvider("websocket"); - this.realtime = this.context.getProvider("realtime"); - - /** - * Register private calls from php for websockets - */ - this.register({ - method: "POST", - url: "/pusher", - handler: (request, reply) => { - const body = request.body as { room: string; data: any }; - const room = body.room; - const data = body.data; - this.ws.getIo().to(room).emit("realtime:event", { name: room, data: data }); - reply.send({}); - }, - }); - - /** - * Register private calls from php channels - */ - this.register({ - method: "GET", - url: "/companies/:company_id/workspaces/:workspace_id/channels/:id/members/:member_id/exists", - handler: (request: FastifyRequest<{ Params: ChannelMemberParameters }>, reply) => { - if (!gr.services) { - reply.code(500).send(); //Server is not ready - return; - } - const membersController = new ChannelMemberCrudController(); - membersController.exists(request, reply); - }, - }); - - /** - * Register private calls from php channels - */ - this.register({ - method: "GET", - url: "/companies/:company_id/workspaces/:workspace_id/channels/:id", - handler: (request: FastifyRequest<{ Params: ChannelParameters }>, reply) => { - if (!gr.services) { - reply.code(500).send(); //Server is not ready - return; - } - const channelsController = new ChannelCrudController(); - channelsController.getForPHP(request, reply); - }, - }); - - /** - * Register private calls from php channels - */ - this.register({ - method: "GET", - url: "/companies/:company_id/channels/:id", - handler: async ( - request: FastifyRequest<{ Params: { company_id: string; id: string } }>, - reply, - ) => { - if (!gr.services) { - reply.code(500).send(); //Server is not ready - return; - } - const workspaces = await gr.services.workspaces.getAllForCompany(request.params.company_id); - - for (const w of workspaces) { - const channel = await gr.services.channels.channels.get( - { - company_id: request.params.company_id, - workspace_id: w.id, - id: request.params.id, - }, - undefined, - ); - if (channel) { - reply.send(channel); - return; - } - } - - reply.code(404).send(); - }, - }); - - /** - * Register private calls from php channels - */ - this.register({ - method: "POST", - url: "/companies/:company_id/workspaces/:workspace_id/channels/defaultchannel", - handler: ( - request: FastifyRequest<{ - Body: CreateChannelBody; - Params: ChannelParameters; - Querystring: { include_users: boolean }; - }>, - reply, - ) => { - if (!gr.services) { - reply.code(500).send(); //Server is not ready - return; - } - const channelsController = new ChannelCrudController(); - request.currentUser = { - id: (request.body as any).user_id, - }; - channelsController.save(request, reply); - }, - }); - - return this; - } -} diff --git a/tdrive/backend/node/src/core/platform/services/phpnode/provider.ts b/tdrive/backend/node/src/core/platform/services/phpnode/provider.ts deleted file mode 100644 index 1a8a2f7a..00000000 --- a/tdrive/backend/node/src/core/platform/services/phpnode/provider.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { FastifyInstance, FastifyRequest, RouteHandlerMethod } from "fastify"; -import { IncomingMessage, ServerResponse, Server } from "http"; -import { TdriveServiceProvider } from "../../framework"; - -export default interface PhpNodeAPI extends TdriveServiceProvider { - accessControl( - request: FastifyRequest, - server: FastifyInstance, - ): Promise; - register(parameters: { method: string; url: string; handler: RouteHandlerMethod }): void; -} diff --git a/tdrive/backend/node/src/core/platform/services/tracker/index.ts b/tdrive/backend/node/src/core/platform/services/tracker/index.ts index cc4cf493..5a6661da 100644 --- a/tdrive/backend/node/src/core/platform/services/tracker/index.ts +++ b/tdrive/backend/node/src/core/platform/services/tracker/index.ts @@ -43,49 +43,9 @@ export default class Tracker extends TdriveService implements Tracke ); }); - const channelCreatedEvent = "channel:created"; - localEventBus.subscribe(channelCreatedEvent, data => { - logger.debug(`Tracker - New ${channelCreatedEvent} event`); - this.track( - { - user: data.user, - event: channelCreatedEvent, - properties: this.getVisibilityObject(data.channel.visibility), - }, - (err: Error) => - err - ? logger.error({ err }, "Tracker - Error while tracking", channelCreatedEvent) - : false, - ); - }); - - const channelMemberCreatedEvent = "channel:member:created"; - localEventBus.subscribe(channelMemberCreatedEvent, data => { - logger.debug(`Tracker - New ${channelMemberCreatedEvent} event`); - this.track( - { - user: data.user, - event: data.user.id !== data.member.user_id ? "channel:invite" : "channel:join", - properties: this.getVisibilityObject(data.channel.visibility), - }, - (err: Error) => - err - ? logger.error({ err }, "Tracker - Error while tracking", channelMemberCreatedEvent) - : false, - ); - }); - return this; } - private getVisibilityObject(visibility: string) { - return { - is_direct: visibility === "direct" ? true : false, - is_private: visibility === "private" ? true : false, - is_public: visibility === "public" ? true : false, - }; - } - public async identify( identity: IdentifyObjectType, callback?: (err: Error) => void, diff --git a/tdrive/backend/node/src/services/channels/entities/channel-activity.ts b/tdrive/backend/node/src/services/channels/entities/channel-activity.ts deleted file mode 100644 index a354c408..00000000 --- a/tdrive/backend/node/src/services/channels/entities/channel-activity.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Type } from "class-transformer"; -import { Channel } from "."; -import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; -import { ChannelStats } from "../services/channel/types"; -import { ChannelType } from "../types"; - -@Entity("channel_activity", { - primaryKey: [["company_id", "workspace_id"], "channel_id"], - type: "channel_activity", -}) -export class ChannelActivity { - // uuid-v4 - @Type(() => String) - @Column("company_id", "string", { generator: "uuid" }) - company_id: string; - - // "uuid-v4" | "direct" - @Type(() => String) - @Column("workspace_id", "string", { generator: "uuid" }) - workspace_id: string | ChannelType.DIRECT; - - // uuid-v4 - @Type(() => String) - @Column("channel_id", "string", { generator: "uuid" }) - channel_id: string; - - @Column("last_activity", "number") - last_activity: number; - - @Column("stats", "encoded_json") - stats: ChannelStats; - - @Column("last_message", "encoded_json") - last_message: - | null - | Record - | { - date: number; - sender: string; - sender_name: string; - title: string; - text: string; - }; - - /* Not stored in database */ - - channel: Channel; - - public getChannelPrimaryKey(): Channel { - if (this.channel) { - return this.channel; - } - const channel = new Channel(); - channel.id = this.channel_id; - channel.workspace_id = this.workspace_id; - channel.company_id = this.company_id; - return channel; - } -} diff --git a/tdrive/backend/node/src/services/channels/entities/channel-counters.ts b/tdrive/backend/node/src/services/channels/entities/channel-counters.ts deleted file mode 100644 index 3958848f..00000000 --- a/tdrive/backend/node/src/services/channels/entities/channel-counters.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; - -export const TYPE = "channel_counters"; - -@Entity(TYPE, { - primaryKey: [["id"], "counter_type", "company_id", "workspace_id"], - type: TYPE, -}) -export class ChannelCounterEntity { - @Column("id", "uuid") - id: string; - - @Column("counter_type", "string") - counter_type: string; - - @Column("company_id", "uuid") - company_id: string; - - @Column("workspace_id", "string") - workspace_id: string; - - @Column("value", "counter") - value: number; -} - -export type ChannelCounterPrimaryKey = Pick< - ChannelCounterEntity, - "id" | "counter_type" | "company_id" | "workspace_id" ->; - -export enum ChannelUserCounterType { - MEMBERS = "members", - MESSAGES = "messages", - GUESTS = "guests", -} diff --git a/tdrive/backend/node/src/services/channels/entities/channel-member-read-cursors.ts b/tdrive/backend/node/src/services/channels/entities/channel-member-read-cursors.ts deleted file mode 100644 index 1054ad4b..00000000 --- a/tdrive/backend/node/src/services/channels/entities/channel-member-read-cursors.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Type } from "class-transformer"; -import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; - -@Entity("channel_member_read_cursors", { - primaryKey: [["company_id"], "channel_id", "user_id"], - type: "channel_member_read_cursors", -}) -export class ChannelMemberReadCursors { - /** - * Primary key - */ - @Type(() => String) - @Column("company_id", "uuid", { generator: "uuid" }) - company_id: string; - - /** - * Primary key - */ - @Type(() => String) - @Column("user_id", "uuid") - user_id: string; - - /** - * Primary key - */ - @Type(() => String) - @Column("channel_id", "uuid") - channel_id: string; - - @Column("read_section", "encoded_json") - read_section: ReadSection; -} - -export type ReadSection = [string, string]; diff --git a/tdrive/backend/node/src/services/channels/entities/channel-member.ts b/tdrive/backend/node/src/services/channels/entities/channel-member.ts deleted file mode 100644 index 77089489..00000000 --- a/tdrive/backend/node/src/services/channels/entities/channel-member.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Type } from "class-transformer"; -import { merge } from "lodash"; -import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; -import { ChannelMemberNotificationLevel, ChannelMemberType } from "../types"; -import { UserObject } from "../../../services/user/web/types"; - -/** - * Defines the member <-> channel link and member settings in channel - * Table name is `user_channels` - */ -@Entity("user_channels", { - primaryKey: [["company_id", "workspace_id"], "user_id", "channel_id"], - type: "user_channels", -}) -export class ChannelMember { - /** - * Primary key - */ - @Type(() => String) - @Column("company_id", "uuid", { generator: "uuid" }) - company_id: string; - - /** - * Primary key - * Null for direct channels - */ - @Type(() => String) - @Column("workspace_id", "string") - workspace_id: string; - - /** - * Primary key - */ - @Type(() => String) - @Column("user_id", "uuid") - user_id: string; - - /** - * Primary key - */ - @Type(() => String) - @Column("channel_id", "uuid") - channel_id: string; - - @Column("type", "string") - type: ChannelMemberType = ChannelMemberType.MEMBER; - - /** - * Timestamp in secconds - */ - @Column("last_access", "number") - last_access: number; - - /** - * When last updated - */ - @Column("last_increment", "number") - last_increment: number; - - /** - * Member marked this channel as favorite? - */ - @Column("favorite", "boolean") - favorite = false; - - /** - * Member defined the notification level for the channel - * Defaults to all - */ - @Column("notification_level", "string") - notification_level: ChannelMemberNotificationLevel = ChannelMemberNotificationLevel.ALL; - - /** - * Member expiration in channel (only for guests) - */ - @Column("expiration", "number") - expiration: boolean | number; - - /** - * Every collection entity must have an id, here it is the user_id - */ - @Type(() => String) - @Column("id", "string") - id: string; - - @Column("updated_at", "number", { onUpsert: _ => new Date().getTime() }) - updated_at: number; - - @Column("created_at", "number", { onUpsert: d => d || new Date().getTime() }) - created_at: number; -} - -export type ChannelMemberPrimaryKey = Pick< - ChannelMember, - "channel_id" | "company_id" | "user_id" | "workspace_id" ->; - -export function getChannelMemberInstance(member: Partial): ChannelMember { - return merge(new ChannelMember(), member); -} - -export type ChannelMemberWithUser = ChannelMember & { user: UserObject }; - -/** - * The channel_members table allows to get all the members of a channel. - * Table name is `channel_members` - */ -@Entity("channel_members", { - primaryKey: [["company_id", "workspace_id"], "channel_id", "user_id"], - type: "channel_members", -}) -export class MemberOfChannel { - /** - * Primary key - */ - @Type(() => String) - @Column("company_id", "uuid", { generator: "uuid" }) - company_id: string; - - @Type(() => String) - @Column("workspace_id", "string") - workspace_id: string; - - @Type(() => String) - @Column("user_id", "uuid") - user_id: string; - - @Type(() => String) - @Column("channel_id", "uuid") - channel_id: string; - - @Column("type", "string") - type: ChannelMemberType = ChannelMemberType.MEMBER; -} - -export function getMemberOfChannelInstance(member: Partial): MemberOfChannel { - return merge(new MemberOfChannel(), member); -} diff --git a/tdrive/backend/node/src/services/channels/entities/channel-pending-emails.ts b/tdrive/backend/node/src/services/channels/entities/channel-pending-emails.ts deleted file mode 100644 index 0baa6197..00000000 --- a/tdrive/backend/node/src/services/channels/entities/channel-pending-emails.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Type } from "class-transformer"; -import { merge } from "lodash"; -import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; - -@Entity("channel_pending_emails", { - primaryKey: [["company_id", "workspace_id"], "channel_id", "email"], - type: "channel_pending_emails", -}) -export class ChannelPendingEmails { - /** - * Primary key - */ - @Type(() => String) - @Column("company_id", "uuid", { generator: "uuid" }) - company_id: string; - - /** - * Primary key - */ - @Type(() => String) - @Column("workspace_id", "uuid") - workspace_id: string; - - /** - * Primary key - */ - @Type(() => String) - @Column("channel_id", "uuid") - channel_id: string; - - /** - * Pending email - */ - @Type(() => String) - @Column("email", "string") - email: string; -} - -export type ChannelPendingEmailsPrimaryKey = Pick< - ChannelPendingEmails, - "channel_id" | "company_id" | "workspace_id" | "email" ->; - -export function getInstance(pendingEmail: ChannelPendingEmails): ChannelPendingEmails { - return merge(new ChannelPendingEmails(), pendingEmail); -} diff --git a/tdrive/backend/node/src/services/channels/entities/channel.search.ts b/tdrive/backend/node/src/services/channels/entities/channel.search.ts deleted file mode 100644 index 78a7d5be..00000000 --- a/tdrive/backend/node/src/services/channels/entities/channel.search.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Channel } from "./channel"; - -export default { - index: "channels", - source: (entity: Channel) => { - return { - workspace_id: entity.workspace_id, - company_id: entity.company_id, - name: entity.channel_group + " " + entity.name, - }; - }, - mongoMapping: { - text: { - name: "text", - }, - }, - esMapping: { - properties: { - name: { type: "text", index_prefixes: { min_chars: 1 } }, - workspace_id: { type: "keyword" }, - company_id: { type: "keyword" }, - }, - }, -}; diff --git a/tdrive/backend/node/src/services/channels/entities/channel.ts b/tdrive/backend/node/src/services/channels/entities/channel.ts deleted file mode 100644 index 86c7bbcb..00000000 --- a/tdrive/backend/node/src/services/channels/entities/channel.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Type } from "class-transformer"; -import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; -import { ChannelType, ChannelVisibility } from "../types"; -import { ChannelMember } from "./channel-member"; -import { UserObject } from "../../user/web/types"; -import { merge } from "lodash"; -import search from "./channel.search"; -import { ChannelActivity } from "./channel-activity"; - -@Entity("channels", { - primaryKey: [["company_id", "workspace_id"], "id"], - type: "channels", - search, -}) -export class Channel { - // uuid-v4 - @Type(() => String) - @Column("company_id", "uuid", { generator: "uuid" }) - company_id: string; - - @Type(() => String) - @Column("workspace_id", "string", { generator: "uuid" }) - workspace_id: string | ChannelType.DIRECT; - - @Type(() => String) - @Column("id", "uuid", { generator: "uuid" }) - id: string; - - @Column("name", "encoded_string") - name: string; - - @Column("icon", "encoded_string") - icon: string; - - @Column("description", "encoded_string") - description: string; - - @Column("channel_group", "encoded_string") - channel_group: string; - - @Column("is_readonly", "boolean") - is_readonly = false; - - @Column("visibility", "encoded_string") - visibility: ChannelVisibility; - - @Column("is_default", "boolean") - is_default = false; - - @Column("archived", "boolean") - archived = false; - - @Column("archivation_date", "number") - archivation_date: number; - - // uuid - @Column("owner", "uuid") - @Type(() => String) - owner: string; - - //This is only used for direct channels - @Column("members", "encoded_json") - members: string[] = []; - - @Column("connectors", "encoded_json") - connectors: string[] = []; //list of app-ids - - @Column("updated_at", "number", { onUpsert: _ => new Date().getTime() }) - updated_at: number; - - @Column("created_at", "number", { onUpsert: d => d || new Date().getTime() }) - created_at: number; - - static isPrivateChannel(channel: Channel): boolean { - return channel.visibility === ChannelVisibility.PRIVATE; - } - - static isPublicChannel(channel: Channel): boolean { - return channel.visibility === ChannelVisibility.PUBLIC; - } - - static isDirectChannel(channel: Channel): boolean { - return ( - channel.visibility === ChannelVisibility.DIRECT || - channel.workspace_id === ChannelVisibility.DIRECT - ); - } - - static isDefaultChannel(channel: Channel): boolean { - return channel?.is_default; - } -} - -export class UserChannel extends Channel { - user_member: ChannelMember; - last_activity: ChannelActivity["last_activity"]; - last_message: ChannelActivity["last_message"]; -} - -export class UsersIncludedChannel extends Channel { - users: UserObject[]; -} - -export function getInstance(channel: Partial): Channel { - return merge(new Channel(), channel); -} - -export type ChannelPrimaryKey = Pick; diff --git a/tdrive/backend/node/src/services/channels/entities/default-channel.ts b/tdrive/backend/node/src/services/channels/entities/default-channel.ts deleted file mode 100644 index 64c7f60a..00000000 --- a/tdrive/backend/node/src/services/channels/entities/default-channel.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Type } from "class-transformer"; -import { merge } from "lodash"; -import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; -import { ChannelType } from "../types"; - -@Entity("default_channels", { - primaryKey: [["company_id", "workspace_id"], "channel_id"], - type: "default_channels", -}) -export class DefaultChannel { - // uuid-v4 - @Type(() => String) - @Column("company_id", "string", { generator: "uuid" }) - company_id: string; - - // "uuid-v4" | "direct" - @Type(() => String) - @Column("workspace_id", "string", { generator: "uuid" }) - workspace_id: string | ChannelType.DIRECT; - - // uuid-v4 - @Type(() => String) - @Column("channel_id", "string", { generator: "uuid" }) - channel_id: string; -} - -export type DefaultChannelPrimaryKey = Pick< - DefaultChannel, - "channel_id" | "company_id" | "workspace_id" ->; - -export function getInstance(channel: DefaultChannel): DefaultChannel { - return merge(new DefaultChannel(), channel); -} diff --git a/tdrive/backend/node/src/services/channels/entities/direct-channel.ts b/tdrive/backend/node/src/services/channels/entities/direct-channel.ts deleted file mode 100644 index 1dd92a35..00000000 --- a/tdrive/backend/node/src/services/channels/entities/direct-channel.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Type } from "class-transformer"; -import { merge } from "lodash"; -import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; - -/** - * Direct Channel information. - * A direct channel is a channel in a company composed of a defined set of users. - */ -@Entity("direct_channels", { - primaryKey: [["company_id"], "users", "channel_id"], - type: "direct_channels", -}) -export class DirectChannel { - /** - * The company identifier of this channel - * uuid-v4 - */ - @Type(() => String) - @Column("company_id", "uuid", { generator: "uuid" }) - company_id: string; - - /** - * The link to Channel.id - * uuid-v4 - */ - @Type(() => String) - @Column("channel_id", "string") - channel_id: string; - - /** - * CSV list of ordered user ids - */ - @Column("users", "string") - users: string; - - static getUsersAsString(users: string[] = []): string { - return users.sort().join(","); - } - - static getUsersFromString(identifier: string = ""): string[] { - return identifier.split(","); - } -} - -export function getInstance(channel: DirectChannel): DirectChannel { - return merge(new DirectChannel(), channel); -} diff --git a/tdrive/backend/node/src/services/channels/entities/index.ts b/tdrive/backend/node/src/services/channels/entities/index.ts deleted file mode 100644 index 41979e79..00000000 --- a/tdrive/backend/node/src/services/channels/entities/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export * from "./channel"; -export * from "./member"; -export * from "./tab"; -export * from "./channel-member"; -export * from "./channel-member-read-cursors"; -export { - ChannelPendingEmails, - ChannelPendingEmailsPrimaryKey, - getInstance as getChannelPendingEmailsInstance, -} from "./channel-pending-emails"; - -export { - DefaultChannel, - DefaultChannelPrimaryKey, - getInstance as getDefaultChannelInstance, -} from "./default-channel"; diff --git a/tdrive/backend/node/src/services/channels/entities/member.ts b/tdrive/backend/node/src/services/channels/entities/member.ts deleted file mode 100644 index 74c52b2b..00000000 --- a/tdrive/backend/node/src/services/channels/entities/member.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Type } from "class-transformer"; - -export class Member { - @Type(() => String) - id: string; -} diff --git a/tdrive/backend/node/src/services/channels/entities/tab.ts b/tdrive/backend/node/src/services/channels/entities/tab.ts deleted file mode 100644 index 01ce1b8c..00000000 --- a/tdrive/backend/node/src/services/channels/entities/tab.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Type } from "class-transformer"; -import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; -import { ChannelType } from "../types"; - -@Entity("channel_tabs", { - primaryKey: [["company_id", "workspace_id"], "channel_id", "id"], - type: "channel_tabs", -}) -export class ChannelTab { - // uuid-v4 - @Type(() => String) - @Column("company_id", "string", { generator: "uuid" }) - company_id: string; - - // "uuid-v4" | "direct" - @Type(() => String) - @Column("workspace_id", "string", { generator: "uuid" }) - workspace_id: string | ChannelType.DIRECT; - - // uuid-v4 - @Type(() => String) - @Column("channel_id", "string", { generator: "uuid" }) - channel_id: string; - - // uuid-v4 - @Type(() => String) - @Column("id", "string", { generator: "uuid" }) - id: string; - - @Column("name", "encoded_string") - name: string; - - @Column("configuration", "encoded_json") - configuration: string; - - @Column("application_id", "encoded_string") - application_id: string; - - @Column("owner", "encoded_string") - owner: string; - - @Column("col_order", "encoded_string") - order: string; - - @Column("updated_at", "number", { onUpsert: _ => new Date().getTime() }) - updated_at: number; - - @Column("created_at", "number", { onUpsert: d => d || new Date().getTime() }) - created_at: number; -} - -export type ChannelTabPrimaryKey = Pick< - ChannelTab, - "company_id" | "workspace_id" | "channel_id" | "id" ->; diff --git a/tdrive/backend/node/src/services/channels/index.ts b/tdrive/backend/node/src/services/channels/index.ts deleted file mode 100644 index e9ad5571..00000000 --- a/tdrive/backend/node/src/services/channels/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Prefix, TdriveService } from "../../core/platform/framework"; -import WebServerAPI from "../../core/platform/services/webserver/provider"; -import web from "./web"; - -@Prefix("/internal/services/channels/v1") -export default class ChannelService extends TdriveService { - version = "1"; - name = "channels"; - - public async doInit(): Promise { - const fastify = this.context.getProvider("webserver").getServer(); - fastify.register((instance, _opts, next) => { - web(instance, { prefix: this.prefix }); - next(); - }); - return this; - } - - // TODO: remove - api(): undefined { - return undefined; - } -} diff --git a/tdrive/backend/node/src/services/channels/services/channel/activities/service.ts b/tdrive/backend/node/src/services/channels/services/channel/activities/service.ts deleted file mode 100644 index 0314e3af..00000000 --- a/tdrive/backend/node/src/services/channels/services/channel/activities/service.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { getLogger, Initializable } from "../../../../../core/platform/framework"; -import { localEventBus } from "../../../../../core/platform/framework/event-bus"; -import { Channel as ChannelEntity, ChannelMember } from "../../../entities/"; -import { ResourceEventsPayload } from "../../../../../utils/types"; -import { ActivityObjectType, ActivityPublishedType } from "./types"; -import _, { sortBy } from "lodash"; -import { MessageQueueServiceAPI } from "../../../../../core/platform/services/message-queue/api"; -import { ChannelParameters } from "../../../web/types"; -import { ChannelVisibility } from "../../../types"; - -const logger = getLogger("channel.activities"); -export default class Activities implements Initializable { - messageQueue: MessageQueueServiceAPI; - - async init(): Promise { - const channelConnectorCreatedEvent = "channel:connector:created"; - const channelConnectorDeletedEvent = "channel:connector:deleted"; - const channelMemberCreatedEvent = "channel:member:created"; - const channelMemberDeletedEvent = "channel:member:deleted"; - const channelTabCreatedEvent = "channel:tab:created"; - const channelTabDeletedEvent = "channel:tab:deleted"; - const channelUpdatedEvent = "channel:updated"; - - localEventBus.subscribe(channelMemberCreatedEvent, data => { - if (data.channel.visibility === ChannelVisibility.DIRECT) { - return; - } - //Fixme: We don't show activity from user himself, - // in the future when we aggregate activities then we could keep it, - // but for now it is polution in the channel - if ((data.resourcesAfter[0] as ChannelMember).user_id === data.actor.id) { - return; - } - this.notify( - channelMemberCreatedEvent, - { - type: "channel:activity:member:created", - actor: { - type: "user", - id: data.actor.id, - }, - context: { - type: "add", - array: [ - // data.actor.id !== data.member.user_id ? "invite" : "join" - { type: "member", resource: data.resourcesAfter[0] }, - ], - }, - }, - data.channel, - ); - }); - - localEventBus.subscribe(channelMemberDeletedEvent, data => { - if (data.channel.visibility === ChannelVisibility.DIRECT) { - return; - } - //Do not notify when user leave the channel by themselves - if (data.resourcesBefore[0].id === data.actor.id) { - return; - } - this.notify( - channelMemberDeletedEvent, - { - type: "channel:activity:member:deleted", - actor: { - type: "user", - id: data.actor.id, - }, - context: { - type: "remove", - array: [{ type: "member", resource: data.resourcesBefore[0] }], - }, - }, - data.channel, - ); - }); - - localEventBus.subscribe(channelUpdatedEvent, data => { - const interestedChanges = ["channel_group", "name", "description", "visibility", "icon"]; - const channelChanged = !_.isEqual( - _.pick(data.resourcesBefore[0], interestedChanges), - _.pick(data.resourcesAfter[0], interestedChanges), - ); - - const connectorsBefore: Partial = _.pick(data.resourcesBefore[0], [ - "connectors", - ]); - - const connectorsAfter: Partial = _.pick(data.resourcesAfter[0], [ - "connectors", - ]); - - const isConnectorCreated: boolean = - connectorsBefore?.connectors.length < connectorsAfter?.connectors.length; - - const channelConnectorsChanged = !_.isEqual( - { connectors: sortBy(connectorsBefore.connectors) }, - { connectors: sortBy(connectorsAfter.connectors) }, - ); - - if (channelConnectorsChanged) { - return localEventBus.publish( - isConnectorCreated ? "channel:connector:created" : "channel:connector:deleted", - data, - ); - } - - if (channelChanged) { - return this.notify( - channelUpdatedEvent, - { - type: "channel:activity:updated", - actor: { - type: "user", - id: data.actor.id, - }, - context: { - type: "diff", - previous: { type: "channel", resource: data.resourcesBefore[0] }, - next: { type: "channel", resource: data.resourcesAfter[0] }, - }, - }, - data.channel, - ); - } - }); - - localEventBus.subscribe(channelTabCreatedEvent, data => - this.notify( - channelTabCreatedEvent, - { - type: "channel:activity:tab:created", - actor: { - type: "user", - id: data.actor.id, - }, - context: { - type: "add", - array: [{ type: "tab", resource: data.resourcesAfter[0] }], - }, - }, - data.channelParameters, - ), - ); - - localEventBus.subscribe(channelTabDeletedEvent, data => - this.notify( - channelTabDeletedEvent, - { - type: "channel:activity:tab:deleted", - actor: { - type: "user", - id: data.actor.id, - }, - context: { - type: "remove", - array: [{ type: "tab", resource: data.resourcesAfter[0] }], - }, - }, - data.channelParameters, - ), - ); - - localEventBus.subscribe(channelConnectorCreatedEvent, data => { - return this.notify( - channelConnectorCreatedEvent, - { - type: "channel:activity:connector:created", - actor: { - type: "user", - id: data.actor.id, - }, - context: { - type: "add", - array: [{ type: "connector", resource: data.resourcesAfter[0] }], - }, - }, - data.channel, - ); - }); - - localEventBus.subscribe(channelConnectorDeletedEvent, data => { - this.notify( - channelConnectorDeletedEvent, - { - type: "channel:activity:connector:deleted", - actor: { - type: "user", - id: data.actor.id, - }, - context: { - type: "remove", - array: [{ type: "connector", resource: data.resourcesBefore[0] }], - }, - }, - data.channel, - ); - }); - - return this; - } - - async notify( - event: string, - data: ActivityObjectType, - channel: ChannelEntity | ChannelParameters, - ): Promise { - logger.debug(`Activities - New ${event} event %o`, data); - try { - await this.messageQueue.publish("channel:activity_message", { - data: { - channel_id: channel.id, - workspace_id: channel.workspace_id, - company_id: channel.company_id, - activity: data, - }, - }); - } catch (err) { - logger.warn({ err }, `Activities - Error while publishing to ${event}`); - } - } -} diff --git a/tdrive/backend/node/src/services/channels/services/channel/activities/types.ts b/tdrive/backend/node/src/services/channels/services/channel/activities/types.ts deleted file mode 100644 index 571fbb60..00000000 --- a/tdrive/backend/node/src/services/channels/services/channel/activities/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Channel, User } from "../../../../../utils/types"; -import { ChannelMember } from "../../../../../services/channels/entities"; - -export type ActorType = { type: string; id: User["id"] }; - -export type ResourceObjectType = { - type: string; - resource: Channel | User | ChannelMember | []; -}; - -export type ContextType = { - type: "add" | "remove" | "diff"; - array?: ResourceObjectType[]; - previous?: ResourceObjectType; - next?: ResourceObjectType; -}; - -export type ActivityObjectType = { - type: string; - actor: ActorType; - context: ContextType; -}; - -export type ActivityPublishedType = { - channel_id: Channel["id"]; - workspace_id: Channel["workspace_id"]; - company_id: Channel["company_id"]; - activity: ActivityObjectType; -}; diff --git a/tdrive/backend/node/src/services/channels/services/channel/default/listener.ts b/tdrive/backend/node/src/services/channels/services/channel/default/listener.ts deleted file mode 100644 index 9f599e1b..00000000 --- a/tdrive/backend/node/src/services/channels/services/channel/default/listener.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Channel, getDefaultChannelInstance } from "../../../entities"; -import { getLogger, Initializable } from "../../../../../core/platform/framework"; -import { localEventBus } from "../../../../../core/platform/framework/event-bus"; -import { ResourceEventsPayload } from "../../../../../utils/types"; -import DefaultChannelServiceImpl from "./service"; - -const logger = getLogger("channel:default:listener"); - -export default class DefaultChannelListener implements Initializable { - constructor(private service: DefaultChannelServiceImpl) {} - - async init(): Promise { - localEventBus.subscribe( - "channel:created", - this.onChannelCreated.bind(this), - ); - localEventBus.subscribe( - "channel:updated", - this.onChannelUpdated.bind(this), - ); - localEventBus.subscribe( - "channel:deleted", - this.onChannelDeleted.bind(this), - ); - return this; - } - - onChannelDeleted(event: ResourceEventsPayload): void { - logger.debug("Channel has been deleted %o", event.channel); - if (!event.channel || !Channel.isDefaultChannel(event.channel)) { - return; - } - - this.service - .delete( - getDefaultChannelInstance({ - channel_id: event.channel.id, - company_id: event.channel.company_id, - workspace_id: event.channel.workspace_id, - }), - ) - .catch((err: Error) => { - logger.error({ err }, "Default channel %id can not be deleted", event.channel.id); - }); - } - - onChannelUpdated(event: ResourceEventsPayload): void { - logger.debug("Channel has been updated %o", event.channel); - - if (!event.channel || !event.resourcesBefore || !event.resourcesBefore.length) { - return; - } - - const channelUpdatedToDefault = - !Channel.isDefaultChannel(event.resourcesBefore[0] as Channel) && - Channel.isDefaultChannel(event.channel); - - const channelIsNotDefaultAnymore = - Channel.isDefaultChannel(event.resourcesBefore[0] as Channel) && - !Channel.isDefaultChannel(event.channel); - - if (channelUpdatedToDefault) { - logger.debug("Channel %s has been switched to 'default'", event.channel.id); - this.service - .create( - getDefaultChannelInstance({ - channel_id: event.channel.id, - company_id: event.channel.company_id, - workspace_id: event.channel.workspace_id, - }), - undefined, - ) - .catch((err: Error) => { - logger.error({ err }, "Default channel %id can not be updated", event.channel.id); - }); - } - - if (channelIsNotDefaultAnymore) { - logger.debug("Channel %s has been switched to !'default'", event.channel.id); - this.service - .delete( - getDefaultChannelInstance({ - channel_id: event.channel.id, - company_id: event.channel.company_id, - workspace_id: event.channel.workspace_id, - }), - ) - .catch((err: Error) => { - logger.error({ err }, "Default channel %id can not be removed", event.channel.id); - }); - } - } - - onChannelCreated(event: ResourceEventsPayload): void { - if (!event.channel || !Channel.isDefaultChannel(event.channel)) { - return; - } - - logger.debug("Channel %s has been created with 'default' attribute", event.channel.id); - this.service - .create( - getDefaultChannelInstance({ - channel_id: event.channel.id, - company_id: event.channel.company_id, - workspace_id: event.channel.workspace_id, - }), - undefined, - ) - .catch((err: Error) => { - logger.warn({ err }, "Default channel %id can not be created", event.channel.id); - }); - } -} diff --git a/tdrive/backend/node/src/services/channels/services/channel/default/service.ts b/tdrive/backend/node/src/services/channels/services/channel/default/service.ts deleted file mode 100644 index 64a65684..00000000 --- a/tdrive/backend/node/src/services/channels/services/channel/default/service.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { concat, EMPTY, from, Observable } from "rxjs"; -import { filter, mergeMap, toArray } from "rxjs/operators"; -import Repository from "../../../../../core/platform/services/database/services/orm/repository/repository"; -import { - Channel, - ChannelMember, - DefaultChannel, - DefaultChannelPrimaryKey, -} from "../../../entities"; -import { - CreateResult, - CrudException, - DeleteResult, - ExecutionContext, - ListResult, - Paginable, - SaveResult, - UpdateResult, -} from "../../../../../core/platform/framework/api/crud-service"; -import { ChannelExecutionContext } from "../../../types"; -import DefaultChannelListener from "./listener"; -import { getLogger } from "../../../../../core/platform/framework"; - -import { User } from "../../../../../utils/types"; -import WorkspaceUser from "../../../../workspaces/entities/workspace_user"; -import gr from "../../../../global-resolver"; - -const logger = getLogger("channel.default"); - -export default class DefaultChannelServiceImpl { - version: "1"; - repository: Repository; - listener: DefaultChannelListener; - - async init(): Promise { - this.repository = await gr.database.getRepository("default_channels", DefaultChannel); - this.listener = new DefaultChannelListener(this); - await this.listener.init(); - return this; - } - - async create( - channel: DefaultChannel, - context: ExecutionContext, - ): Promise> { - await this.repository.save(channel, context); - - // Once a default channel has been successfully created, we have to add all the workspace users as member of the channel - // There are several ways to do it: Directly or using pubsub - // Since member service is a channel sub service we can do it directly - // Another good way to be sure that the user is in default channels is to have an endpoint which ensures this... - this.onCreated(channel); - return new CreateResult("default_channel", channel); - } - - get(pk: DefaultChannelPrimaryKey, context: ExecutionContext): Promise { - return this.repository.findOne(pk, {}, context); - } - - update( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - pk: DefaultChannelPrimaryKey, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - item: DefaultChannel, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - context?: ChannelExecutionContext, - ): Promise> { - throw new Error("Method not implemented."); - } - - save?( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - item: DefaultChannel, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - options?: SaveOptions, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - context?: ChannelExecutionContext, - ): Promise> { - throw new Error("Method not implemented."); - } - - async delete( - pk: DefaultChannelPrimaryKey, - context?: ExecutionContext, - ): Promise> { - const defaultChannel = await this.get(pk, context); - - if (!defaultChannel) { - throw CrudException.notFound("Default channel has not been found"); - } - - await this.repository.remove(defaultChannel, context); - - return new DeleteResult("default_channel", defaultChannel, true); - } - - list( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - pagination: Paginable, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - options?: ListOptions, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - context?: ChannelExecutionContext, - ): Promise> { - throw new Error("Method not implemented."); - } - - /** - * @param channel - */ - onCreated(channel: DefaultChannel): void { - logger.debug("Default channel %s: Adding workspace users as members", channel.channel_id); - const members: ChannelMember[] = []; - const subscription = this.addWorkspaceUsersToChannel(channel).subscribe({ - next: member => { - logger.debug( - "Default Channel %s: User %s has been added: %s", - channel.channel_id, - member.user.userId, - member.added, - ); - member.added && members.push(member.member); - }, - error: (err: Error) => { - logger.error({ err }, "Default Channel %s: Error while adding users", channel.channel_id); - }, - complete: () => { - logger.debug( - "Default Channel %s: Workspace users have been added: %o", - channel.channel_id, - members.map(m => m.user_id), - ); - subscription.unsubscribe(); - }, - }); - } - - addWorkspaceUsersToChannel( - channel: DefaultChannelPrimaryKey, - ): Observable<{ user?: WorkspaceUser; member?: ChannelMember; added: boolean; err?: Error }> { - const workspaceUsers$ = gr.services.workspaces.getAllUsers$({ - workspaceId: channel.workspace_id, - }); - - return workspaceUsers$.pipe( - // filter out external workspace users - filter(wsUser => !wsUser.isExternal), - mergeMap(wsUser => - from( - gr.services.channels.members - .addUserToChannels({ id: wsUser.userId }, [ - { - company_id: channel.company_id, - workspace_id: channel.workspace_id, - id: channel.channel_id, - } as Channel, - ]) - .then(result => ({ - user: wsUser, - member: result.getEntities()[0].member, - added: result.getEntities()[0].added, - err: result.getEntities()[0].err, - })) - .catch(err => { - return { user: wsUser, added: false, err }; - }), - ), - ), - ); - } - - async addUserToDefaultChannels( - user: User, - workspace: Required>, - ): Promise> { - if (!user || !workspace) { - throw new Error("Can not add user to default channel (user or workspaces are empty)"); - } - - try { - const companyUser = await gr.services.companies.getCompanyUser( - { id: workspace.company_id }, - user, - ); - - if (!companyUser) { - return []; - } - - // Do not add guest in default channels - if (companyUser.role === "guest") { - logger.debug( - "CompanyUser role is %s, not added in workspace %s", - companyUser.role, - workspace.workspace_id, - ); - return []; - } - - const channels = await this.getDefaultChannels({ - company_id: workspace.company_id, - workspace_id: workspace.workspace_id, - }); - - if (!channels || !channels.length) { - logger.debug("No default channels in workspace %s", workspace.workspace_id); - return []; - } - - logger.info("Adding user %s to channels %s", user, JSON.stringify(channels)); - - const regChannels = channels.map(c => ({ id: c.channel_id, ...c } as any as Channel)); - - return ( - await gr.services.channels.members.addUserToChannels(user, regChannels) - ).getEntities(); - } catch (err) { - logger.error({ err }, "Error while adding user for default channels"); - throw new Error("Error while adding user for default channels"); - } - } - - getDefaultChannels( - workspace: Pick, - pagination?: Paginable, - context?: ExecutionContext, - ): Promise { - return this.getDefaultChannels$(workspace, pagination, context).pipe(toArray()).toPromise(); - } - - getDefaultChannels$( - workspace: Pick, - pagination?: Paginable, - context?: ExecutionContext, - ): Observable { - const list = ( - workspace: Pick, - pagination: Paginable, - ) => { - return this.repository.find( - workspace, - { - pagination: { limitStr: pagination?.limitStr, page_token: pagination?.page_token }, - }, - context, - ); - }; - - return from(list(workspace, pagination)).pipe( - mergeMap(channels => { - const items$ = from(channels.getEntities()); - const next$ = channels?.nextPage?.page_token - ? this.getDefaultChannels$(workspace, channels.nextPage) - : EMPTY; - - return concat(items$, next$); - }), - ); - } -} diff --git a/tdrive/backend/node/src/services/channels/services/channel/pending-emails/service.ts b/tdrive/backend/node/src/services/channels/services/channel/pending-emails/service.ts deleted file mode 100644 index d3959550..00000000 --- a/tdrive/backend/node/src/services/channels/services/channel/pending-emails/service.ts +++ /dev/null @@ -1,180 +0,0 @@ -import Repository from "../../../../../core/platform/services/database/services/orm/repository/repository"; -import { - CreateResult, - CrudException, - DeleteResult, - ExecutionContext, - ListResult, - Pagination, - SaveResult, - UpdateResult, -} from "../../../../../core/platform/framework/api/crud-service"; -import { ChannelExecutionContext } from "../../../types"; -import { getLogger } from "../../../../../core/platform/framework"; -import { - Channel, - ChannelPendingEmails, - ChannelPendingEmailsPrimaryKey, - ChannelPrimaryKey, - getChannelPendingEmailsInstance, -} from "../../../entities"; -import { ChannelPendingEmailsListQueryParameters } from "../../../web/types"; -import { plainToClass } from "class-transformer"; -import { NewUserInWorkspaceNotification } from "../types"; -import gr from "../../../../global-resolver"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const logger = getLogger("channel.pending_emails"); - -export default class ChannelPendingEmailServiceImpl { - version: "1"; - repository: Repository; - - async init(): Promise { - this.repository = await gr.database.getRepository( - "channel_pending_emails", - ChannelPendingEmails, - ); - - return this; - } - - async create( - pendingEmail: ChannelPendingEmails, - context: ExecutionContext, - ): Promise> { - await this.repository.save(pendingEmail, context); - - // Once a channel pending email has been successfully created, we have to add him to all channels of the company that he was invited - //this.onCreated(pendingEmail); - return new CreateResult("channel_pending_emails", pendingEmail); - } - - get( - pk: ChannelPendingEmailsPrimaryKey, - context: ExecutionContext, - ): Promise { - return this.repository.findOne(pk, {}, context); - } - - update( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - pk: ChannelPendingEmailsPrimaryKey, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - item: ChannelPendingEmails, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - context?: ChannelExecutionContext, - ): Promise> { - throw new Error("Method not implemented."); - } - - save?( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - item: ChannelPendingEmails, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - options?: SaveOptions, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - context?: ChannelExecutionContext, - ): Promise> { - throw new Error("Method not implemented."); - } - - async delete( - pk: ChannelPendingEmailsPrimaryKey, - context?: ExecutionContext, - ): Promise> { - const pendingEmail = await this.get(pk, context); - - if (!pendingEmail) { - throw CrudException.notFound("Channel pendingEmail has not been found"); - } - - await this.repository.remove(pendingEmail, context); - - return new DeleteResult("channel_pending_emails", pendingEmail, true); - } - - async list( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - pagination: Pagination, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - options?: ListOptions, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - context?: ChannelExecutionContext, - ): Promise> { - const pk = { - company_id: context.channel.company_id, - workspace_id: context.channel.workspace_id, - channel_id: context.channel.id, - }; - - const result = await this.repository.find(pk, { pagination }, context); - - return new ListResult( - "channel_pending_emails", - result - .getEntities() - .map(pendingMember => - plainToClass(ChannelPendingEmails, { email: pendingMember.email, ...pendingMember }), - ), - result.nextPage, - ); - } - - findPendingEmails( - pk: ChannelPendingEmailsListQueryParameters, - context: ExecutionContext, - ): Promise> { - return this.repository.find(pk, {}, context); - } - - async proccessPendingEmails( - user: NewUserInWorkspaceNotification, - workspace: Required>, - context: ExecutionContext, - ): Promise { - // Get user object - const userObj = await gr.services.users.get({ - id: user.user_id, - }); - - // All pending emails in workspace - const allPendingEmailsInWorkspace = await this.repository.find(workspace, {}, context); - - // Filter pending emails in workspace with user object email - allPendingEmailsInWorkspace.filterEntities(({ email }) => email === userObj.email_canonical); - - // Add user to all channel that he is invited then delete pending email entity - allPendingEmailsInWorkspace - .getEntities() - .forEach(async ({ workspace_id, channel_id, company_id, email }) => { - // Add user to channel - const list = await gr.services.channels.members.addUserToChannels(userObj, [ - { - workspace_id, - company_id, - id: channel_id, - } as Channel, - ]); - - // If added to channel, delete pending email - const isAddedToChannel = list.getEntities()[0].added; - if (isAddedToChannel) { - await this.delete( - getChannelPendingEmailsInstance({ - workspace_id, - company_id, - email, - channel_id, - }), - ); - } - }); - } - - onCreated( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - pendingEmail: ChannelPendingEmails, - ): void { - throw new Error("Method not implemented."); - } -} diff --git a/tdrive/backend/node/src/services/channels/services/channel/realtime.ts b/tdrive/backend/node/src/services/channels/services/channel/realtime.ts deleted file mode 100644 index 745ebdd3..00000000 --- a/tdrive/backend/node/src/services/channels/services/channel/realtime.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { User, WebsocketMetadata, Workspace } from "../../../../utils/types"; -import { Channel } from "../../entities"; -import { WorkspaceExecutionContext } from "../../types"; -import { isDirectChannel } from "../../utils"; - -export function getWebsocketInformation(channel: Channel): WebsocketMetadata { - return { - name: channel.name, - room: getChannelPath(channel), - }; -} - -export function getWorkspaceRooms(workspace: Workspace, user: User): WebsocketMetadata[] { - return isDirectChannel(workspace) - ? [{ room: getDirectChannelRoomName(workspace, user.id) }] - : [{ room: getPublicRoomName(workspace) }, { room: getPrivateRoomName(workspace, user) }]; -} - -export function getDirectChannelRoomName(workspace: Workspace, userId: string): string { - return `/companies/${workspace.company_id}/workspaces/direct/channels?type=direct&user=${userId}`; -} - -export function getDirectChannelRooms(workspace: Workspace, channel: Channel): string[] { - return (channel.members || []).map(member => getDirectChannelRoomName(workspace, member)); -} - -export function getPrivateRoomName(workspace: Workspace, user: User): string { - return `/companies/${workspace.company_id}/workspaces/${workspace.workspace_id}/channels?type=private&user=${user.id}`; -} - -export function getPublicRoomName(workspace: Workspace): string { - return `/companies/${workspace?.company_id}/workspaces/${workspace?.workspace_id}/channels?type=public`; -} - -export function getRoomName(channel: Channel, context?: WorkspaceExecutionContext): string[] { - return isDirectChannel(channel) - ? getDirectChannelRooms(context.workspace, channel) - : [getPublicRoomName(context.workspace)]; -} - -export function getChannelPath( - channel: Pick, - context?: WorkspaceExecutionContext, -): string { - return `${getChannelsPath(context?.workspace)}/${channel.id}`; -} - -export function getChannelsPath(workspace?: Workspace): string { - return workspace - ? `/companies/${workspace.company_id}/workspaces/${workspace.workspace_id}/channels` - : "/channels"; -} diff --git a/tdrive/backend/node/src/services/channels/services/channel/service.ts b/tdrive/backend/node/src/services/channels/services/channel/service.ts deleted file mode 100644 index aeae4ed6..00000000 --- a/tdrive/backend/node/src/services/channels/services/channel/service.ts +++ /dev/null @@ -1,987 +0,0 @@ -import _, { cloneDeep, find } from "lodash"; -import { diff } from "deep-object-diff"; -import { - getLogger, - RealtimeDeleted, - RealtimeSaved, - RealtimeUpdated, -} from "../../../../core/platform/framework"; -import { - CrudException, - DeleteResult, - ExecutionContext, - ListResult, - OperationType, - Pagination, - SaveResult, - UpdateResult, -} from "../../../../core/platform/framework/api/crud-service"; -import { ChannelActivityMessage, ChannelObject, SearchChannelOptions } from "./types"; -import { - Channel, - ChannelMember, - ChannelPrimaryKey, - DefaultChannel, - UserChannel, - UsersIncludedChannel, -} from "../../entities"; -import { getChannelPath, getRoomName } from "./realtime"; -import { ChannelType, ChannelVisibility, WorkspaceExecutionContext } from "../../types"; -import { isWorkspaceAdmin as userIsWorkspaceAdmin } from "../../../../utils/workspace"; -import { ResourceEventsPayload, User } from "../../../../utils/types"; -import { pick } from "../../../../utils/pick"; -import { - DirectChannel, - getInstance as getDirectChannelInstance, -} from "../../entities/direct-channel"; -import { ChannelListOptions, ChannelSaveOptions } from "../../web/types"; -import { isDirectChannel } from "../../utils"; -import { ResourcePath } from "../../../../core/platform/services/realtime/types"; -import Repository, { - FindFilter, -} from "../../../../core/platform/services/database/services/orm/repository/repository"; -import { ChannelActivity } from "../../entities/channel-activity"; -import { localEventBus } from "../../../../core/platform/framework/event-bus"; -import DefaultChannelServiceImpl from "./default/service"; -import { formatUser } from "../../../../utils/users"; -import gr from "../../../global-resolver"; -import { - KnowledgeGraphEvents, - KnowledgeGraphGenericEventPayload, -} from "../../../../core/platform/services/knowledge-graph/types"; -import { ChannelUserCounterType } from "../../entities/channel-counters"; -import { Readable } from "stream"; -import sharp from "sharp"; - -const logger = getLogger("channel.service"); - -export class ChannelServiceImpl { - version: "1"; - activityRepository: Repository; - channelRepository: Repository; - directChannelRepository: Repository; - defaultChannelService: DefaultChannelServiceImpl; - - async init(): Promise { - this.defaultChannelService = new DefaultChannelServiceImpl(); - - try { - this.activityRepository = await gr.database.getRepository( - "channel_activity", - ChannelActivity, - ); - this.channelRepository = await gr.database.getRepository("channels", Channel); - this.directChannelRepository = await gr.database.getRepository( - "direct_channels", - DirectChannel, - ); - } catch (err) { - logger.error({ err }, "Can not initialize channel db service"); - } - - try { - await this.defaultChannelService.init(); - } catch (err) { - logger.warn("Can not initialize default channel service", err); - } - - localEventBus.subscribe("workspace:user:deleted", async data => { - if (data?.user?.id && data?.company?.id) - gr.services.channels.members.ensureUserNotInWorkspaceIsNotInChannel( - data.user, - data.workspace, - undefined, - ); - }); - - return this; - } - - async thumbnail(channelId: string, context?: ExecutionContext): Promise<{ file: Readable }> { - const logoInternalPath = `/channels/${channelId}/thumbnail.jpg`; - const file = await gr.platformServices.storage.read(logoInternalPath, {}, context); - return { file }; - } - - @RealtimeSaved((channel, context) => [ - { - room: ResourcePath.get(getRoomName(channel, context as WorkspaceExecutionContext)), - path: getChannelPath(channel, context as WorkspaceExecutionContext), - }, - ]) - async save( - channel: Channel, - options: ChannelSaveOptions, - context: WorkspaceExecutionContext, - ): Promise> { - let channelToUpdate: Channel; - let channelToSave: Channel; - const mode = channel.id ? OperationType.UPDATE : OperationType.CREATE; - const isDirectChannel = Channel.isDirectChannel(channel); - const isWorkspaceAdmin = - !isDirectChannel && (await userIsWorkspaceAdmin(context.user, context.workspace)); - const isPrivateChannel = Channel.isPrivateChannel(channel); - const isDefaultChannel = Channel.isDefaultChannel(channel); - - channel.workspace_id = context.workspace.workspace_id || channel.workspace_id; - channel.company_id = context.workspace.company_id || channel.company_id; - - if (isDirectChannel) { - channel.visibility = ChannelVisibility.DIRECT; - channel.workspace_id = ChannelVisibility.DIRECT; - } - - if (mode === OperationType.UPDATE) { - logger.debug("Updating channel"); - channelToUpdate = await this.get(this.getPrimaryKey(channel), context); - - if (!channelToUpdate) { - throw CrudException.notFound("Channel not found"); - } - - const isChannelOwner = this.isChannelOwner(channelToUpdate, context.user); - const updatableParameters: Partial> = { - name: true, - description: !isDirectChannel, - icon: !isDirectChannel, - channel_group: !isDirectChannel, - is_default: (isWorkspaceAdmin || isChannelOwner) && !isDirectChannel && !isPrivateChannel, - visibility: !isDirectChannel && (isWorkspaceAdmin || isChannelOwner), - archived: isWorkspaceAdmin || isChannelOwner, - connectors: !isDirectChannel, - is_readonly: isWorkspaceAdmin || isChannelOwner, - }; - - // Diff existing channel and input one, cleanup all the undefined fields for all objects - const channelDiff = diff(channelToUpdate, channel); - const fields = Object.keys(channelDiff) as Array>; - - if (!fields.length) { - throw CrudException.badRequest("Nothing to update"); - } - - const updatableFields = fields.filter(field => updatableParameters[field]); - - if (!updatableFields.length) { - throw CrudException.badRequest("Current user can not update requested fields"); - } - - channelToSave = cloneDeep(channelToUpdate); - - updatableFields.forEach(field => { - if (channel[field] !== undefined) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (channelToSave as any)[field] = channel[field]; - } - }); - - localEventBus.publish("channel:updated", { - actor: context.user, - resourcesBefore: [channelToUpdate], - resourcesAfter: [channel], - channel: channel, - user: context.user, - }); - } - - if (mode === OperationType.CREATE) { - if (isPrivateChannel && isDefaultChannel) { - throw CrudException.badRequest("Private channel can not be default"); - } - - if (isDirectChannel) { - options.members = Array.from(new Set(options?.members || []).add(context.user.id)); - channel.members = options.members; - - logger.info("Direct channel creation with members %o", options.members); - if (context.workspace.workspace_id !== ChannelVisibility.DIRECT) { - throw CrudException.badRequest("Direct Channel creation error: bad workspace"); - } - - const directChannel = await this.getDirectChannelInCompany( - context.workspace.company_id, - options.members, - context, - ); - - if (directChannel) { - logger.debug("Direct channel already exists %o", directChannel); - const existingChannel = await this.channelRepository.findOne( - { - company_id: context.workspace.company_id, - workspace_id: ChannelType.DIRECT, - id: directChannel.channel_id, - }, - {}, - context, - ); - if (existingChannel) { - const last_activity = await this.getChannelActivity(existingChannel, context); - - const saveResult = new SaveResult( - "channels", - ChannelObject.mapTo(existingChannel, { last_activity }), - OperationType.EXISTS, - ); - await this.addContextUserToChannel(context, saveResult); - - return saveResult; - } else { - //Fixme: remove directChannel instance - throw CrudException.badRequest("table inconsistency"); - } - } - } else { - if (!channel.name) { - throw CrudException.badRequest("'name' is required"); - } - } - - channelToSave = channel; - channelToSave.owner = context.user.id; - } - - if (channelToSave.icon && channelToSave.icon.startsWith("data:")) { - const logoInternalPath = `/channels/${channelToSave.id}/thumbnail.jpg`; - const logoPublicPath = `/internal/services/channels/v1/companies/${ - channelToSave.company_id - }/workspaces/${channelToSave.workspace_id}/channels/${ - channelToSave.id - }/thumbnail?t=${new Date().getTime()}`; - - const image = await sharp(Buffer.from(channel.icon.split(",").pop(), "base64")) - .resize(250, 250) - .toBuffer(); - - const s = new Readable(); - s.push(image); - s.push(null); - await gr.platformServices.storage.write(logoInternalPath, s); - - channelToSave.icon = logoPublicPath; - } - - const channelActivity = await this.activityRepository.findOne( - { - channel_id: channel.id, - }, - {}, - context, - ); - - logger.info("Saving channel %o", channelToSave); - await this.channelRepository.save(channelToSave, context); - - if (!isDirectChannel) { - channel.members = []; //Members is specific to direct channels - } - - const saveResult = new SaveResult( - "channel", - { - ...ChannelObject.mapTo(channelToSave), - last_activity: channelActivity?.last_activity || 0, - last_message: channelActivity?.last_message || {}, - }, - mode, - ); - - await this.addContextUserToChannel(context, saveResult); - await this.onSaved(channelToSave, options, context, saveResult, mode); - - // Shortcut to invite members to a channel - if (!isDirectChannel && options.members && options.members.length > 0) { - await gr.services.channels.members.addUsersToChannel( - options.members.map(id => { - return { id }; - }), - saveResult.entity, - context, - ); - } - - return saveResult; - } - - async get(pk: ChannelPrimaryKey, _context?: ExecutionContext): Promise { - const primaryKey = this.getPrimaryKey(pk); - let channel = await this.channelRepository.findOne(primaryKey); - if (!channel) { - channel = await this.channelRepository.findOne({ ...primaryKey, workspace_id: "direct" }); - } - if (!channel) return null; - - const last_activity = await this.getChannelActivity(channel); - - if (channel.visibility === ChannelVisibility.DIRECT) - channel = await this.includeUsersInDirectChannel(channel); - - return ChannelObject.mapTo(channel, { last_activity }); - } - - @RealtimeUpdated((channel, context) => [ - { - room: ResourcePath.get(getRoomName(channel, context as WorkspaceExecutionContext)), - path: getChannelPath(channel, context as WorkspaceExecutionContext), - }, - ]) - async update( - pk: ChannelPrimaryKey, - channel: Channel, - context: ExecutionContext, - ): Promise> { - // TODO: Do the update by hand then save - if (!pk.id) { - throw CrudException.badRequest("Channel id is required for update"); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mergeChannel: any = { ...channel, ...pk }; - await this.channelRepository.save(mergeChannel as Channel, context); - - return new UpdateResult("channel", ChannelObject.mapTo(mergeChannel)); - } - - @RealtimeDeleted((channel, context) => [ - { - room: ResourcePath.get(getRoomName(channel, context as WorkspaceExecutionContext)), - path: getChannelPath(channel, context as WorkspaceExecutionContext), - }, - ]) - async delete( - pk: ChannelPrimaryKey, - context: WorkspaceExecutionContext, - ): Promise> { - const channelToDelete = await this.channelRepository.findOne( - this.getPrimaryKey(pk), - {}, - context, - ); - - if (!channelToDelete) { - throw new CrudException("Channel not found", 404); - } - - const directChannel = isDirectChannel(channelToDelete); - - if (directChannel) { - throw new CrudException("Direct channel can not be deleted", 400); - } - - const isWorkspaceAdmin = - !directChannel && (await userIsWorkspaceAdmin(context.user, context.workspace)); - const isChannelOwner = this.isChannelOwner(channelToDelete, context.user); - - if (!isWorkspaceAdmin && !isChannelOwner) { - throw new CrudException("Channel can not be deleted", 400); - } - - await this.channelRepository.remove(channelToDelete, context); - const result = new DeleteResult("channel", ChannelObject.mapTo(pk as Channel), true); - - this.onDeleted(channelToDelete, result); - - localEventBus.publish("channel:deleted", { - channel: channelToDelete, - user: context.user, - }); - - return result; - } - - @RealtimeUpdated((channelActivity, context) => { - return [ - { - room: ResourcePath.get( - getRoomName(channelActivity.getChannelPrimaryKey(), context as WorkspaceExecutionContext), - ), - path: getChannelPath( - channelActivity.getChannelPrimaryKey(), - context as WorkspaceExecutionContext, - ), - resource: { - company_id: channelActivity.company_id, - workspace_id: channelActivity.workspace_id, - id: channelActivity.channel_id, - last_activity: channelActivity.last_activity, - last_message: channelActivity.last_message, - }, - }, - ]; - }) - async updateLastActivity( - payload: { - date: number; - channel: ChannelPrimaryKey; - message: ChannelActivityMessage | null; - }, - context: WorkspaceExecutionContext, - ): Promise> { - const channelPK = payload.channel; - const channelActivityMessage = payload.message; - const channel = (await this.channelRepository.findOne( - _.pick(channelPK, "company_id", "workspace_id", "id"), - {}, - context, - )) as ChannelObject & { stats: ChannelObject["stats"] }; - await gr.services.channels.channels.completeWithStatistics([channel]); - - const entity = new ChannelActivity(); - entity.channel_id = channelPK.id; - entity.company_id = channelPK.company_id; - entity.workspace_id = channelPK.workspace_id; - entity.last_activity = payload.date; - entity.stats = channel.stats; - entity.last_message = channelActivityMessage - ? { - date: channelActivityMessage.date, - sender: channelActivityMessage.sender, - sender_name: channelActivityMessage.sender_name, - title: channelActivityMessage.title, - text: channelActivityMessage.text, - } - : null; - - entity.channel = channel; - - logger.info(`Update activity for channel ${entity.channel_id} to ${entity.last_activity}`); - - localEventBus.publish>( - KnowledgeGraphEvents.CHANNEL_UPSERT, - { - id: channel.id, - resource: channel, - links: [], - }, - ); - - await this.activityRepository.save(entity, context); - return new UpdateResult("channel_activity", entity); - } - - public async getChannelActivity(channel: Channel, context?: ExecutionContext): Promise { - let result = 0; - - if (!channel) { - return result; - } - - try { - const activity = await this.activityRepository.findOne( - { - company_id: channel.company_id, - workspace_id: channel.workspace_id, - channel_id: channel.id, - } as ChannelActivity, - {}, - context, - ); - - result = activity?.last_activity || 0; - } catch (error) { - logger.debug(`Can not get channel last activity for channel ${channel.id}`); - } - return result; - } - - public async fillChannelActivities( - channels: Channel[], - context: ExecutionContext, - ): Promise { - const filledChannels: ChannelObject[] = []; - for (const channel of channels) { - const activity = await this.getChannelActivity(channel, context); - const chObj = ChannelObject.mapTo(channel); - chObj.last_activity = activity; - filledChannels.push(chObj); - } - return filledChannels; - } - - async list( - pagination: Pagination, - options: ChannelListOptions, - context: WorkspaceExecutionContext, - ): Promise> { - const isDirectWorkspace = isDirectChannel(context.workspace); - const isWorkspaceAdmin = - !isDirectWorkspace && userIsWorkspaceAdmin(context.user, context.workspace); - const findFilters: FindFilter = { - company_id: context.workspace.company_id, - workspace_id: context.workspace.workspace_id, - }; - - let user_id = context.user.id; - if (context.user.application_id) { - user_id = user_id || options.user_id; - } - - if ((options?.mine || isDirectWorkspace) && user_id) { - if (!context.user.application_id && !context.user.server_request) { - localEventBus.publish("channel:list", { - user: context.user, - company: { - id: context.workspace.company_id, - }, - }); - } - - return this.getChannelsForUsersInWorkspace( - context.workspace.company_id, - context.workspace.workspace_id, - context.user.id, - pagination, - context, - ); - } - - const channels = await this.channelRepository.find(findFilters, { pagination }, context); - channels.filterEntities(channel => channel.visibility !== ChannelVisibility.DIRECT); - - if (!isWorkspaceAdmin && !context.user.server_request) { - channels.filterEntities(channel => channel.visibility === ChannelVisibility.PUBLIC); - } - - return channels; - } - - async getChannelsForUsersInWorkspace( - companyId: string, - workspaceId: string | "direct", - userId: string, - pagination?: Pagination, - context?: ExecutionContext, - ): Promise> { - const isDirectWorkspace = isDirectChannel({ workspace_id: workspaceId }); - const findFilters: FindFilter = { - company_id: companyId, - workspace_id: workspaceId, - }; - - const userChannels = await gr.services.channels.members.listUserChannels( - { id: userId }, - pagination, - { - user: { - id: userId, - }, - workspace: { - workspace_id: workspaceId, - company_id: companyId, - }, - }, - ); - - let activityPerChannel: Map; - const channels = await this.channelRepository.find( - findFilters, - { - $in: [["id", userChannels.getEntities().map(channelMember => channelMember.channel_id)]], - }, - context, - ); - - if (!channels.isEmpty()) { - const activities = await this.activityRepository.find( - findFilters, - { - $in: [["channel_id", channels.getEntities().map(channel => channel.id)]], - }, - context, - ); - - activityPerChannel = new Map( - activities.getEntities().map(activity => [activity.channel_id, activity]), - ); - } else { - activityPerChannel = new Map(); - } - - channels.mapEntities((channel: Channel) => { - const userChannel = find(userChannels.getEntities(), { channel_id: channel.id }); - - const channelActivity = activityPerChannel.get(channel.id); - - return { - ...channel, - ...{ user_member: userChannel }, - last_activity: channelActivity?.last_activity || 0, - last_message: channelActivity?.last_message || {}, - } as unknown as UserChannel; - }); - - if (isDirectWorkspace) { - channels.mapEntities((channel: UserChannel) => { - return { - ...channel, - ...{ - members: channel.members || [], - }, - } as unknown as UserDirectChannel; - }); - } - - return new ListResult( - channels.type, - channels.getEntities() as UserChannel[], - userChannels.nextPage, - ); - } - - async createDirectChannel( - directChannel: DirectChannel, - context: ExecutionContext, - ): Promise { - const instance = getDirectChannelInstance(directChannel); - await this.directChannelRepository.save(instance, context); - - return instance; - } - - getDirectChannel( - directChannel: DirectChannel, - context: ExecutionContext, - ): Promise { - return this.directChannelRepository.findOne(directChannel, {}, context); - } - - async getDirectChannelInCompany( - companyId: string, - users: string[], - context: ExecutionContext, - ): Promise { - const directChannel = await this.directChannelRepository.findOne( - { - company_id: companyId, - users: DirectChannel.getUsersAsString(users), - }, - {}, - context, - ); - - return directChannel; - } - - async getDirectChannelsInCompany( - pagination: Pagination, - company_id: string, - context: ExecutionContext, - ): Promise> { - return await this.channelRepository.find( - { company_id, workspace_id: "direct" }, - { pagination }, - context, - ); - } - - async getDirectChannelsForUsersInCompany( - companyId: string, - userId: string, - context: ExecutionContext, - ): Promise { - const list = await this.directChannelRepository.find( - { - company_id: companyId, - }, - { - $like: [["users", userId]], - }, - context, - ); - - return list.getEntities(); - } - - async markAsRead( - pk: ChannelPrimaryKey, - user: Pick, - context: ExecutionContext, - ): Promise { - const now = Date.now(); - const channel = await this.get(pk, context); - - if (!channel) { - throw CrudException.notFound("Channel not found"); - } - - await this.completeWithStatistics([channel]); - - const member = await gr.services.channels.members.getChannelMember( - user, - channel, - undefined, - context, - ); - - if (!member) { - throw CrudException.badRequest("User is not channel member"); - } - - // Updating the member will also publish a message in the message-queue channel - // This message will be handled in the notification service and will update the notification preferences for the member - // cf this.members.onUpdated - member.last_access = now; - member.last_increment = channel.stats.messages; - const updatedMember = ( - await gr.services.channels.members.save(member, { - channel, - user, - }) - ).entity; - - this.onRead(channel, updatedMember); - - return true; - } - - async markAsUnread( - pk: ChannelPrimaryKey, - user: User, - context: ExecutionContext, - ): Promise { - const channel = await this.get(pk, context); - - if (!channel) { - throw CrudException.notFound("Channel not found"); - } - - const member = await gr.services.channels.members.getChannelMember(user, channel); - - if (!member) { - throw CrudException.badRequest("User is not channel member"); - } - - // do nothing here but send a notification so that notification service is updated... - this.onUnread(channel, member); - - return true; - } - - getDefaultChannels( - workspace: Required>, - ): Promise { - return this.defaultChannelService.getDefaultChannels(workspace); - } - - async addUserToDefaultChannels( - user: User, - workspace: Required>, - ): Promise { - const result = await this.defaultChannelService.addUserToDefaultChannels(user, workspace); - - return result.filter(e => e.added).map(e => e.member); - } - - getPrimaryKey(channelOrPrimaryKey: Channel | ChannelPrimaryKey): ChannelPrimaryKey { - return pick(channelOrPrimaryKey, ...(["company_id", "workspace_id", "id"] as const)); - } - - isChannelOwner(channel: Channel, user: User): boolean { - return channel.owner && String(channel.owner) === user.id; - } - - async addContextUserToChannel( - context: WorkspaceExecutionContext, - result: SaveResult, - ): Promise { - const savedChannel = result.entity; - - //Add requester as member - if (context.user.id) { - try { - await gr.services.channels.members.addUserToChannels({ id: context.user.id }, [ - savedChannel, - ]); - } catch (err) { - logger.warn({ err }, "Can not add requester as channel member"); - } - } - } - - public async includeUsersInDirectChannel( - channel: Channel, - excludeUserId?: string, - ): Promise { - const channelWithUsers: UsersIncludedChannel = { users: [], ...channel }; - if (isDirectChannel(channel)) { - const users = []; - for (const user of channel.members) { - if (user) { - const e = await formatUser(await gr.services.users.getCached({ id: user })); - if (e) users.push(e); - } - } - channelWithUsers.users = users; - channelWithUsers.name = users - .filter(u => u.id != excludeUserId) - .map(u => u.full_name?.trim()) - .filter(a => a) - .join(", "); - } - return channelWithUsers; - } - - /** - * Called when channel update has been successfully called - * - * @param channel The channel before update has been processed - * @param result The channel update result - */ - async onSaved( - channel: Channel, - options: ChannelSaveOptions, - context: WorkspaceExecutionContext, - result: SaveResult, - mode: OperationType.CREATE | OperationType.UPDATE, - ): Promise { - const savedChannel = result.entity; - - if (mode === OperationType.CREATE) { - if (isDirectChannel(channel)) { - const directChannel = { - channel_id: savedChannel.id, - company_id: savedChannel.company_id, - users: DirectChannel.getUsersAsString(options.members), - } as DirectChannel; - - await this.createDirectChannel(directChannel, context); - } else { - if (options.addCreatorAsMember && savedChannel.owner) { - try { - await gr.services.channels.members.addUserToChannels({ id: savedChannel.owner }, [ - savedChannel, - ]); - } catch (err) { - logger.warn({ err }, "Can not add owner as channel member"); - } - } - } - - this.updateLastActivity( - { - date: new Date().getTime(), - channel: channel, - message: null, - }, - context, - ); - - localEventBus.publish("channel:created", { - channel, - user: context.user, - }); - } - } - - /** - * Called when channel delete has been successfully called - * - * @param channel The channel to delete - * @param result The delete result - */ - onDeleted(channel: Channel, result: DeleteResult): void { - if (result.deleted) { - logger.debug("Channel %s has been deleted", channel.id); - } - } - - /** - * Called when a channel as been marked as read. - * Will publish `channel:read` notification in the message-queue service - * - * @param channel - * @param member - */ - onRead(channel: Channel, member: ChannelMember): void { - logger.info(`Channel ${channel.id} as been marked as read for user ${member.id}`); - - gr.platformServices.messageQueue.publish("channel:read", { - data: { - channel, - member, - }, - }); - } - - /** - * Called when a channel as been marked as unread. - * Will publish `channel:unread` notification in the message-queue service - * - * @param channel - * @param member - */ - onUnread(channel: Channel, member: ChannelMember): void { - logger.info(`Channel ${channel.id} as been marked as unread for user ${member.id}`); - - gr.platformServices.messageQueue.publish("channel:unread", { - data: { - channel, - member, - }, - }); - } - - async search( - pagination: Pagination, - options: SearchChannelOptions, - context: ExecutionContext, - ): Promise> { - const rep = gr.platformServices.search.getRepository("channel", Channel); - return rep.search( - {}, - { - pagination, - ...(options.companyId ? { $in: [["company_id", [options.companyId]]] } : {}), - $text: { - $search: options.search, - }, - }, - context, - ); - } - - async getAllChannelsInWorkspace( - company_id: string, - workspace_id: string, - context?: ExecutionContext, - ): Promise { - let pagination = new Pagination(null, "100"); - - const channels: Channel[] = []; - do { - const res = await this.channelRepository.find( - { - company_id: company_id, - workspace_id: workspace_id, - }, - { - pagination, - }, - context, - ); - pagination = new Pagination(res.nextPage.page_token, res.nextPage.limitStr); - channels.push(...res.getEntities()); - } while (pagination.page_token); - - return channels; - } - - async completeWithStatistics( - channels: Pick[], - ) { - await Promise.all( - channels.map(async a => { - const members = await gr.services.channels.members.getUsersCount({ - ..._.pick(a, "id", "company_id", "workspace_id"), - counter_type: ChannelUserCounterType.MEMBERS, - }); - //Fixme: even if it works strange to use "getUsersCount" to get messages count - const messages = await gr.services.channels.members.getUsersCount({ - ..._.pick(a, "id", "company_id", "workspace_id"), - counter_type: ChannelUserCounterType.MESSAGES, - }); - a.stats = { members, messages }; - }), - ); - } -} diff --git a/tdrive/backend/node/src/services/channels/services/channel/types.ts b/tdrive/backend/node/src/services/channels/services/channel/types.ts deleted file mode 100644 index 76a47855..00000000 --- a/tdrive/backend/node/src/services/channels/services/channel/types.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { merge } from "lodash"; -import { Channel } from "../../entities/channel"; -import { ChannelActivity } from "../../entities/channel-activity"; -import { ChannelMember } from "../../entities"; -import { UserObject } from "../../../user/web/types"; - -export type NewUserInWorkspaceNotification = { - user_id: string; - company_id: string; - workspace_id: string; -}; - -export type ChannelStats = { - members: number; - messages: number; -}; - -type ChannelType = "workspace" | "direct"; - -export class ChannelMemberObject extends ChannelMember { - id: string; - user_id: string; - // type: "member" | "guest" | "bot"; - last_access: number; //Timestamp in seconds - last_increment: number; - favorite: boolean; //Did the user add this channel to its favorites - // notification_level: "all" | "none" | "group_mentions" | "user_mentions"; - - static mapTo( - channelMember: ChannelMember, - channelMemberLikeObject: Partial = {}, - ): ChannelMemberObject { - if (!channelMember) { - return null; - } - - return merge(new ChannelMemberObject(), { - favorite: false, - last_increment: 0, - last_access: 0, - ...channelMember, - ...channelMemberLikeObject, - }); - } -} - -export class ChannelObject extends Channel { - last_activity?: ChannelActivity["last_activity"]; - last_message?: ChannelActivity["last_message"]; - default: boolean; - type: ChannelType; - user_member: ChannelMemberObject; - users: UserObject[]; - stats: ChannelStats; - - constructor() { - super(); - } - - static mapTo(channel: Channel, channelLikeObject: Partial = {}): ChannelObject { - if (!channel) { - return null; - } - - return merge(new ChannelObject(), { - ...{ last_activity: 0 }, - ...channel, - ...channelLikeObject, - ...{ members: channel?.members?.length ? channel.members : [] }, - default: channel.is_default, - }); - } -} - -export type SearchChannelOptions = { - search: string; - companyId?: string; -}; - -export type ChannelActivityMessage = { - date: number; - sender: string; - sender_name: string; - title: string; - text: string; -}; diff --git a/tdrive/backend/node/src/services/channels/services/member/realtime.ts b/tdrive/backend/node/src/services/channels/services/member/realtime.ts deleted file mode 100644 index ea6f99eb..00000000 --- a/tdrive/backend/node/src/services/channels/services/member/realtime.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Channel, User, WebsocketMetadata } from "../../../../utils/types"; -import { ChannelMember } from "../../entities"; -import { getChannelPath, getChannelsPath } from "../channel/realtime"; -import { ChannelExecutionContext } from "../../types"; - -export function getPrivateRoomName(channel: Channel, user: User): string { - return `/companies/${channel.company_id}/workspaces/${channel.workspace_id}/channels/${channel.id}?type=private&user=${user.id}`; -} - -export function getPublicRoomName(channel: Channel): string { - return `/companies/${channel.company_id}/workspaces/${channel.workspace_id}/channels/${channel.id}?type=public`; -} - -export function getRoomName(channelOrContext: Channel | ChannelExecutionContext): string { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const channel = (channelOrContext as any).channel - ? (channelOrContext as ChannelExecutionContext).channel - : (channelOrContext as Channel); - - return getPublicRoomName(channel); -} - -export function getMembersPath(channel: Channel): string { - return `${getChannelsPath(channel)}/members`; -} - -export function getMemberPath(member: ChannelMember, context?: ChannelExecutionContext): string { - const parentContext = { - workspace: { - company_id: context?.channel?.company_id, - workspace_id: context?.channel?.workspace_id, - }, - user: context?.user, - }; - - return `${getChannelPath(context.channel, parentContext)}/members/${member.user_id}`; -} - -export function getChannelRooms(channel: Channel, user: User): WebsocketMetadata[] { - return [{ room: getPublicRoomName(channel) }, { room: getPrivateRoomName(channel, user) }]; -} diff --git a/tdrive/backend/node/src/services/channels/services/member/service.ts b/tdrive/backend/node/src/services/channels/services/member/service.ts deleted file mode 100644 index f16aadbd..00000000 --- a/tdrive/backend/node/src/services/channels/services/member/service.ts +++ /dev/null @@ -1,911 +0,0 @@ -import { getLogger, RealtimeDeleted, RealtimeSaved } from "../../../../core/platform/framework"; -import { - CreateResult, - CrudException, - DeleteResult, - ExecutionContext, - ListResult, - OperationType, - Pagination, - SaveResult, - UpdateResult, -} from "../../../../core/platform/framework/api/crud-service"; - -import { - Channel as ChannelEntity, - ChannelMember, - ChannelMemberPrimaryKey, - ChannelMemberReadCursors, - getChannelMemberInstance, - getMemberOfChannelInstance, - MemberOfChannel, - ReadSection, -} from "../../entities"; -import { - ChannelExecutionContext, - ChannelMemberType, - ChannelVisibility, - WorkspaceExecutionContext, -} from "../../types"; -import { Channel, ResourceEventsPayload, User } from "../../../../utils/types"; -import { cloneDeep, isNil, omitBy } from "lodash"; -import { updatedDiff } from "deep-object-diff"; -import { pick } from "../../../../utils/pick"; -import { getMemberPath, getRoomName } from "./realtime"; -import { ChannelListOptions } from "../../web/types"; -import { ResourcePath } from "../../../../core/platform/services/realtime/types"; -import Repository from "../../../../core/platform/services/database/services/orm/repository/repository"; -import { localEventBus } from "../../../../core/platform/framework/event-bus"; -import { plainToClass } from "class-transformer"; -import { - ChannelCounterEntity, - ChannelCounterPrimaryKey, - ChannelUserCounterType, - TYPE as ChannelCounterEntityType, -} from "../../entities/channel-counters"; -import { CounterProvider } from "../../../../core/platform/services/counter/provider"; -import { countRepositoryItems } from "../../../../utils/counters"; -import NodeCache from "node-cache"; -import { UserPrimaryKey } from "../../../user/entities/user"; -import { WorkspacePrimaryKey } from "../../../workspaces/entities/workspace"; - -import gr from "../../../global-resolver"; -import uuidTime from "uuid-time"; -import { ChannelObject } from "../channel/types"; -import { CompanyExecutionContext } from "../../../applications/web/types"; -const USER_CHANNEL_KEYS = [ - "id", - "company_id", - "workspace_id", - "user_id", - "channel_id", - "type", - "last_access", - "last_increment", - "favorite", - "notification_level", - "expiration", -] as const; - -const CHANNEL_MEMBERS_KEYS = [ - "company_id", - "workspace_id", - "user_id", - "channel_id", - "type", -] as const; - -const logger = getLogger("channel.member"); - -export class MemberServiceImpl { - version: "1"; - userChannelsRepository: Repository; - channelMembersRepository: Repository; - channelMembersReadCursorRepository: Repository; - private channelCounter: CounterProvider; - private cache: NodeCache; - - async init(): Promise { - try { - this.userChannelsRepository = await gr.database.getRepository("user_channels", ChannelMember); - this.channelMembersRepository = await gr.database.getRepository( - "channel_members", - MemberOfChannel, - ); - this.channelMembersReadCursorRepository = await gr.database.getRepository( - "channel_members_read_cursors", - ChannelMemberReadCursors, - ); - } catch (err) { - logger.error({ err }, "Can not initialize channel member service"); - } - - const channelCountersRepository = await gr.database.getRepository( - ChannelCounterEntityType, - ChannelCounterEntity, - ); - - this.channelCounter = await gr.platformServices.counter.getCounter( - channelCountersRepository, - ); - - this.channelCounter.setReviseCallback(async (pk: ChannelCounterPrimaryKey) => { - if (pk.counter_type === ChannelUserCounterType.MESSAGES) { - return; - } - const type = - ChannelUserCounterType.MEMBERS === pk.counter_type - ? ChannelMemberType.MEMBER - : ChannelMemberType.GUEST; - return countRepositoryItems( - this.channelMembersRepository, - { channel_id: pk.id, company_id: pk.company_id, workspace_id: pk.workspace_id }, - { type }, - ); - }, 400); - - this.cache = new NodeCache({ stdTTL: 1, checkperiod: 120 }); - - return this; - } - - @RealtimeSaved((member, context) => { - return [ - //Send member preferences update to channels collections - { - room: `/companies/${member.company_id}/workspaces/${member.workspace_id}/channels?type=${ - member.workspace_id === ChannelVisibility.DIRECT ? "direct" : "private" - }&user=${member.user_id}`, - resource: { - company_id: member.company_id, - workspace_id: member.workspace_id, - id: member.channel_id, - user_member: member, - }, - }, - { - room: ResourcePath.get(getRoomName(context as ChannelExecutionContext)), - path: getMemberPath(member, context as ChannelExecutionContext), - }, - ]; - }) - async save( - member: ChannelMember, - context: ChannelExecutionContext, - ): Promise> { - let memberToSave: ChannelMember; - const channel = await gr.services.channels.channels.get(context.channel, context); - - if (!channel) { - throw CrudException.notFound("Channel does not exists"); - } - - const memberToUpdate = await this.userChannelsRepository.findOne( - this.getPrimaryKey(member), - {}, - context, - ); - const mode = memberToUpdate ? OperationType.UPDATE : OperationType.CREATE; - - logger.debug(`MemberService.save - ${mode} member %o`, memberToUpdate); - - if (mode === OperationType.UPDATE) { - const isCurrentUser = this.isCurrentUser(memberToUpdate, context.user); - - if (!isCurrentUser) { - throw CrudException.badRequest(`Channel member ${member.user_id} can not be updated`); - } - - const updatableParameters: Partial> = { - notification_level: isCurrentUser, - favorite: isCurrentUser, - last_access: isCurrentUser, - last_increment: isCurrentUser, - }; - - // Diff existing channel and input one, cleanup all the undefined fields for all objects - const memberDiff = omitBy(updatedDiff(memberToUpdate, member), isNil); - const fields = Object.keys(memberDiff) as Array>; - - if (!fields.length) { - return new SaveResult("channel_member", member, mode); - } - - const updatableFields = fields.filter(field => updatableParameters[field]); - - if (!updatableFields.length) { - throw CrudException.badRequest("Current user can not update requested fields"); - } - - memberToSave = cloneDeep(memberToUpdate); - - //Ensure it is a boolean - memberToSave.favorite = !!memberToSave.favorite; - - updatableFields.forEach(field => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (memberToSave as any)[field] = member[field]; - }); - - await this.saveChannelMember(memberToSave, context); - this.onUpdated( - context.channel, - memberToSave, - new UpdateResult("channel_member", memberToSave), - ); - } else { - const currentUserIsMember = !!(await this.getChannelMember( - context.user, - channel, - undefined, - context, - )); - const isPrivateChannel = ChannelEntity.isPrivateChannel(channel); - const isPublicChannel = ChannelEntity.isPublicChannel(channel); - const isDirectChannel = ChannelEntity.isDirectChannel(channel); - const userIsDefinedInChannelUserList = (channel.members || []).includes( - String(member.user_id), - ); - const isChannelCreator = context.user && String(channel.owner) === String(context.user.id); - - // 1. Private channel: user can not join private channel by themself when it is private - // only member can add other users in channel - // 2. The channel creator check is only here on channel creation - if ( - isChannelCreator || - isPublicChannel || - context.user.server_request || - (isPrivateChannel && currentUserIsMember) || - (isDirectChannel && userIsDefinedInChannelUserList) - ) { - const memberToSave = { ...member, ...context.channel }; - await this.saveChannelMember(memberToSave, context); - - await this.usersCounterIncrease(channel, member.user_id); - this.onCreated( - channel, - member, - context.user, - new CreateResult("channel_member", memberToSave), - ); - } else { - throw CrudException.badRequest( - `User ${member.user_id} is not allowed to join this channel`, - ); - } - } - - return new SaveResult("channel_member", member, mode); - } - - async get(pk: ChannelMemberPrimaryKey, context: ExecutionContext): Promise { - // FIXME: Who can fetch a single member? - return await this.userChannelsRepository.findOne(this.getPrimaryKey(pk), {}, context); - } - - @RealtimeDeleted((member, context) => [ - { - room: `/companies/${member.company_id}/workspaces/${member.workspace_id}/channels?type=${ - member.workspace_id === ChannelVisibility.DIRECT ? "direct" : "private" - }&user=${member.user_id}`, - resource: { - company_id: member.company_id, - workspace_id: member.workspace_id, - id: member.channel_id, - user_id: member.user_id, - user_member: null, - }, - }, - { - room: ResourcePath.get(getRoomName(context as ChannelExecutionContext)), - path: getMemberPath(member, context as ChannelExecutionContext), - }, - ]) - async delete( - pk: ChannelMemberPrimaryKey, - context: ChannelExecutionContext, - ): Promise> { - const memberToDelete = await this.userChannelsRepository.findOne(pk, {}, context); - const channel = await gr.services.channels.channels.get(context.channel, context); - - if (!channel) { - throw CrudException.notFound("Channel does not exists"); - } - - if (!memberToDelete) { - throw CrudException.notFound("Channel member not found"); - } - - if (ChannelEntity.isDirectChannel(channel)) { - if (!this.isCurrentUser(memberToDelete, context.user)) { - throw CrudException.badRequest("User can not remove other users from direct channel"); - } - } - - if (ChannelEntity.isPrivateChannel(channel)) { - const canLeave = await this.canLeavePrivateChannel(context.user, channel); - - if (!canLeave) { - throw CrudException.badRequest("User can not leave the private channel"); - } - } - - await this.userChannelsRepository.remove(getChannelMemberInstance(pk), context); - await this.channelMembersRepository.remove(getMemberOfChannelInstance(pk), context); - await this.usersCounterIncrease(channel, pk.user_id, -1); - - this.onDeleted(memberToDelete, context.user, channel); - - return new DeleteResult("channel_member", pk as ChannelMember, true); - } - - /** - * List given channel members - */ - async list( - pagination: Pagination, - options: ChannelListOptions, - context: ChannelExecutionContext, - ): Promise> { - const channel = await gr.services.channels.channels.get( - { - company_id: context.channel.company_id, - workspace_id: context.channel.workspace_id, - id: context.channel.id, - }, - context, - ); - - if (!channel) { - throw CrudException.notFound("Channel not found"); - } - - if (ChannelEntity.isDirectChannel(channel) || ChannelEntity.isPrivateChannel(channel)) { - const isMember = await this.getChannelMember(context.user, channel, undefined, context); - - if (!isMember && !context.user.application_id && !context.user.server_request) { - throw CrudException.badRequest("User does not have enough rights to get channels"); - } - } - - const result = await this.channelMembersRepository.find( - { - company_id: context.channel.company_id, - workspace_id: context.channel.workspace_id, - channel_id: context.channel.id, - }, - { pagination }, - context, - ); - - const companyUsers = await gr.services.companies.getUsers( - { - group_id: context.channel.company_id, - }, - new Pagination(), - { userIds: result.getEntities().map(member => member.user_id) }, - ); - - if (options.company_role) { - companyUsers.filterEntities(entity => entity.role === options.company_role); - } - - const companyUserIds = companyUsers.getEntities().map(entity => entity.user_id); - - result.filterEntities(cm => companyUserIds.includes(cm.user_id)); - - return new ListResult( - "channel_member", - result - .getEntities() - .map(member => plainToClass(ChannelMember, { id: member.user_id, ...member })), - result.nextPage, - ); - } - - async listAllUserChannelsIds( - user_id: string, - company_id: string, - workspace_id: string, - context: ExecutionContext, - ): Promise { - let pagination = new Pagination(null); - const channels: string[] = []; - let result: ListResult; - let hasMoreItems = true; - do { - result = await this.userChannelsRepository.find( - { - company_id, - workspace_id, - user_id, - }, - { pagination }, - context, - ); - if (result.isEmpty()) { - hasMoreItems = false; - } else { - result.getEntities().forEach(entity => { - channels.push(entity.channel_id); - }); - if (!result.nextPage.page_token) hasMoreItems = false; - else pagination = new Pagination(result.nextPage.page_token); - } - } while (hasMoreItems); - return channels; - } - - async listUserChannels( - user: User, - pagination: Pagination, - context: WorkspaceExecutionContext, - ): Promise> { - const result = await this.userChannelsRepository.find( - { - company_id: context.workspace.company_id, - workspace_id: context.workspace.workspace_id, - user_id: user.id, - }, - { pagination }, - context, - ); - - return new ListResult( - "channel_member", - result - .getEntities() - .map(member => plainToClass(ChannelMember, { id: member.user_id, ...member })), - result.nextPage, - ); - } - - /** - * Can leave only if the number of members is > 1 - * since counting rows in DB takes too much time - * we query a list for 2 members without offset and check the length - */ - async canLeavePrivateChannel(user: User, channel: ChannelEntity): Promise { - const list = this.list(new Pagination(undefined, "2"), {}, { channel, user }); - - return (await list).getEntities().length > 1; - } - - async addUsersToChannel( - users: Pick[] = [], - channel: ChannelEntity & { stats: ChannelObject["stats"] }, - context?: ExecutionContext, - ): Promise< - ListResult<{ channel: ChannelEntity; added: boolean; member?: ChannelMember; err?: Error }> - > { - if (!channel) { - throw CrudException.badRequest("Channel is required"); - } - logger.debug( - "Add users %o to channel %o", - users.map(u => u.id), - channel.id, - ); - - await gr.services.channels.channels.completeWithStatistics([channel]); - - const members: Array<{ - channel: ChannelEntity; - added: boolean; - member?: ChannelMember; - err?: Error; - }> = await Promise.all( - users.map(async user => { - const channelContext: ChannelExecutionContext = { - channel, - user: context?.user || user, - }; - - const member: ChannelMember = getChannelMemberInstance({ - channel_id: channel.id, - company_id: channel.company_id, - workspace_id: channel.workspace_id, - user_id: user.id, - last_increment: channel.stats.messages, - last_access: Date.now(), - } as ChannelMember); - - try { - const isAlreadyMember = await this.getChannelMember(user, channel, undefined, context); - if (isAlreadyMember) { - logger.debug("User %s is already member in channel %s", member.user_id, channel.id); - return { channel, added: false }; - } - - const result = await this.save(member, channelContext); - - return { - channel, - added: true, - member: result.entity, - }; - } catch (err) { - logger.warn({ err }, "Member has not been added %o", member); - return { - channel, - err, - added: false, - }; - } - }), - ); - - return new ListResult("channel_member", members); - } - - async addUserToChannels( - user: Pick, - channels: ChannelEntity[], - ): Promise< - ListResult<{ channel: ChannelEntity; added: boolean; member?: ChannelMember; err?: Error }> - > { - logger.debug( - "Add user %s to channels %o", - user.id, - channels.map(c => c.id), - ); - const members: Array<{ - channel: ChannelEntity; - added: boolean; - member?: ChannelMember; - err?: Error; - }> = await Promise.all( - channels.map(async channel => { - const context: ChannelExecutionContext = { - channel, - user, - }; - - const member = getChannelMemberInstance({ - channel_id: channel.id, - company_id: channel.company_id, - workspace_id: channel.workspace_id, - user_id: user.id, - }); - - try { - const isAlreadyMember = await this.getChannelMember(user, channel, undefined, context); - if (isAlreadyMember) { - logger.debug("User %s is already member in channel %s", member.user_id, channel.id); - return { channel, added: false }; - } - - const result = await this.save(member, context); - - return { channel, member: result.entity, added: true }; - } catch (err) { - logger.warn({ err }, "Member has not been added %o", member); - return { channel, added: false, err }; - } - }), - ); - - return new ListResult("channel_member", members); - } - - onUpdated( - channel: Channel, - member: ChannelMember, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - updateResult: UpdateResult, - ): void { - logger.debug("Member updated %o", member); - - gr.platformServices.messageQueue.publish("channel:member:updated", { - data: { - channel, - member, - }, - }); - } - - onCreated( - channel: ChannelEntity, - member: ChannelMember, - user: User, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - createResult: SaveResult, - ): void { - logger.debug("Member created %o", member); - - gr.platformServices.messageQueue.publish("channel:member:created", { - data: { - channel, - user, - member, - }, - }); - - localEventBus.publish("channel:member:created", { - channel, - user, - member, - actor: user, - resourcesAfter: [member], - }); - } - - onDeleted(member: ChannelMember, user: User, channel: ChannelEntity): void { - logger.debug("Member deleted %o", member); - - gr.platformServices.messageQueue.publish("channel:member:deleted", { - data: { - channel, - user, - member, - }, - }); - - localEventBus.publish("channel:member:deleted", { - actor: user, - resourcesBefore: [member], - channel, - user, - member, - }); - } - - isCurrentUser(member: ChannelMember | MemberOfChannel, user: User): boolean { - return String(member.user_id) === String(user.id); - } - - async getChannelMember( - user: User, - channel: Partial>, - cacheTtlSec?: number, - context?: ExecutionContext, - ): Promise { - if (!user) { - return; - } - - if (cacheTtlSec) { - const pk = JSON.stringify({ user, channel }); - if (this.cache.has(pk)) return this.cache.get(pk); - const entity = await this.getChannelMember(user, channel, undefined, context); - this.cache.set(pk, entity, cacheTtlSec); - return entity; - } - - return this.get( - { - channel_id: channel.id, - company_id: channel.company_id, - workspace_id: channel.workspace_id, - user_id: user.id, - }, - context, - ); - } - - getPrimaryKey( - memberOrPrimaryKey: ChannelMember | ChannelMemberPrimaryKey, - ): ChannelMemberPrimaryKey { - return pick( - memberOrPrimaryKey, - ...(["company_id", "workspace_id", "channel_id", "user_id"] as const), - ); - } - - private async usersCounterIncrease(channel: Channel, userId: string, increaseValue: number = 1) { - // const isMember = await this.isCompanyMember(channel.company_id, userId); - const isMember = true; // Since actually all users are now added to the channel as members, we are removing the guest counter for now. - const counter_type = isMember ? ChannelUserCounterType.MEMBERS : ChannelUserCounterType.GUESTS; - return this.channelCounter.increase( - { - id: channel.id, - company_id: channel.company_id, - workspace_id: channel.workspace_id, - counter_type, - }, - increaseValue, - ); - } - - getUsersCount(pk: ChannelCounterPrimaryKey): Promise { - return this.channelCounter.get(pk); - } - - async isCompanyMember(companyId: string, userId: string): Promise { - return (await gr.services.companies.getUserRole(companyId, userId)) != "guest"; - } - - private async saveChannelMember( - memberToSave: ChannelMember & Channel, - context: ExecutionContext, - ) { - const userChannel = getChannelMemberInstance(pick(memberToSave, ...USER_CHANNEL_KEYS)); - const channelMember = getMemberOfChannelInstance(pick(memberToSave, ...CHANNEL_MEMBERS_KEYS)); - await this.userChannelsRepository.save(userChannel, context); - await this.channelMembersRepository.save(channelMember, context); - } - - async ensureUserNotInWorkspaceIsNotInChannel( - userPk: UserPrimaryKey, - workspacePk: WorkspacePrimaryKey, - context: ExecutionContext, - ) { - const workspace = await gr.services.workspaces.get(workspacePk); - const member = await gr.services.workspaces.getUser({ - workspaceId: workspace.id, - userId: userPk.id, - }); - if (!member) { - const result = await this.userChannelsRepository.find( - { - company_id: workspace.company_id, - workspace_id: workspace.id, - user_id: userPk.id, - }, - {}, - context, - ); - for (const channel of result.getEntities()) { - logger.warn( - `User ${userPk.id} is not in workspace ${workspace.id} so it will be removed from channel ${channel.id}`, - ); - await this.delete(channel, { user: { id: userPk.id }, channel }); - } - } - } - - /** - * The list of members read sections of the channel. - * - * @param {ChannelExecutionContext} context - The context of the execution. - * @returns {Promise>} - The channel members read cursors. - */ - async getChannelMembersReadSections( - context: ChannelExecutionContext, - ): Promise> { - try { - const readCursors = await this.channelMembersReadCursorRepository.find( - { - company_id: context.channel.company_id, - channel_id: context.channel.id, - }, - { pagination: { limitStr: "100" } }, - context, - ); - - return new ListResult( - "channel_members_read_cursors", - readCursors.getEntities(), - ); - } catch (error) { - logger.error(error); - throw error; - } - } - - /** - * The channel member read section of the channel. - * - * @param {ChannelExecutionContext} context - The context of the execution. - * @returns {Promise} - The channel member read cursors. - */ - async getChannelMemberReadSections( - member: string, - context: ChannelExecutionContext, - ): Promise { - try { - const readCursors = await this.channelMembersReadCursorRepository.findOne( - { - company_id: context.channel.company_id, - channel_id: context.channel.id, - user_id: member, - }, - {}, - context, - ); - - return readCursors; - } catch (error) { - logger.error(error); - throw error; - } - } - - /** - * Updates the channel member read section. - * - * @param {{ start: string; end: string }} section - The section to save the cursor for. - * @param {CompanyExecutionContext & { channel_id: string }} context - The channel execution context. - * @returns {Promise>} - The result of the save operation. - */ - async setChannelMemberReadSections( - section: { start: string; end: string }, - context: CompanyExecutionContext & { channel_id: string; workspace_id: string }, - ): Promise> { - const member = await this.getChannelMember( - context.user, - { - id: context.channel_id, - company_id: context.company.id, - workspace_id: context.workspace_id, - }, - null, - context, - ); - - if (!member) { - throw CrudException.badRequest( - `User ${context.user.id} is not a member of channel ${context.channel_id}`, - ); - } - - const existingReadSection = await this.channelMembersReadCursorRepository.findOne( - { - company_id: context.company.id, - channel_id: context.channel_id, - user_id: context.user.id, - }, - {}, - context, - ); - - const { start, end } = section; - - if ( - !existingReadSection || - !existingReadSection.read_section || - !existingReadSection.read_section.length - ) { - const newMemberReadCursor = new ChannelMemberReadCursors(); - newMemberReadCursor.company_id = context.company.id; - newMemberReadCursor.channel_id = context.channel_id; - newMemberReadCursor.user_id = context.user.id; - newMemberReadCursor.read_section = [start, end] as ReadSection; - - await this.channelMembersReadCursorRepository.save(newMemberReadCursor, context); - - return new SaveResult( - "channel_members_read_cursors", - newMemberReadCursor, - OperationType.CREATE, - ); - } - - const updatedReadSection = { start, end }; - const [existingStart, existingEnd] = existingReadSection.read_section; - const existingStartTime = uuidTime.v1(existingStart); - const existingEndTime = uuidTime.v1(existingEnd); - const startTime = uuidTime.v1(start); - const endTime = uuidTime.v1(end); - - if (existingStartTime < startTime) { - updatedReadSection.start = existingStart; - } - - if (existingEndTime > endTime) { - updatedReadSection.end = existingEnd; - } - - if (existingStart === updatedReadSection.start && existingEnd === updatedReadSection.end) { - return new SaveResult( - "channel_members_read_cursors", - existingReadSection, - OperationType.EXISTS, - ); - } - - existingReadSection.read_section = [updatedReadSection.start, updatedReadSection.end]; - await this.channelMembersReadCursorRepository.save(existingReadSection, context); - - return new SaveResult( - "channel_members_read_cursors", - existingReadSection, - OperationType.UPDATE, - ); - } - - /** - * list users who have seen the message. - * - * @param {String} id - the message id - * @param {ChannelExecutionContext} context - the thread execution context - * @returns { Promise} - the promise containing the user id list - */ - async getChannelMessageSeenByUsers( - id: string, - context: ChannelExecutionContext, - ): Promise { - const channelReadSections = await this.getChannelMembersReadSections(context); - - return channelReadSections - .getEntities() - .filter(section => { - const sectionEnd = section.read_section[1]; - const sectionEndTime = uuidTime.v1(sectionEnd); - const messageTime = uuidTime.v1(id); - - return messageTime <= sectionEndTime; - }) - .map(section => section.user_id); - } -} diff --git a/tdrive/backend/node/src/services/channels/services/pubsub/index.ts b/tdrive/backend/node/src/services/channels/services/pubsub/index.ts deleted file mode 100644 index 383e0cba..00000000 --- a/tdrive/backend/node/src/services/channels/services/pubsub/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Initializable } from "../../../../core/platform/framework"; -import { NewChannelActivityProcessor } from "./new-channel-activity"; -import { NewUserInWorkspaceJoinDefaultChannelsProcessor } from "./new-user-in-workspace-join-default-channels"; -import { NewPendingEmailsInWorkspaceJoinChannelsProcessor } from "./new-pending-emails-in-workspace-join-channels"; -import { NewWorkspaceProcessor } from "./new-workspace"; -import gr from "../../../global-resolver"; - -export class ChannelsMessageQueueListener implements Initializable { - async init(): Promise { - const channelActivityProcessor = await new NewChannelActivityProcessor().init(); - gr.platformServices.messageQueue.processor.addHandler(channelActivityProcessor); - gr.platformServices.messageQueue.processor.addHandler( - new NewUserInWorkspaceJoinDefaultChannelsProcessor(), - ); - gr.platformServices.messageQueue.processor.addHandler( - new NewPendingEmailsInWorkspaceJoinChannelsProcessor(), - ); - gr.platformServices.messageQueue.processor.addHandler(new NewWorkspaceProcessor()); - - return this; - } -} diff --git a/tdrive/backend/node/src/services/channels/services/pubsub/new-channel-activity.ts b/tdrive/backend/node/src/services/channels/services/pubsub/new-channel-activity.ts deleted file mode 100644 index 127b2bd4..00000000 --- a/tdrive/backend/node/src/services/channels/services/pubsub/new-channel-activity.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { getLogger } from "../../../../core/platform/framework"; -import { MessageQueueHandler } from "../../../../core/platform/services/message-queue/api"; -import { ChannelActivityNotification } from "../../types"; -import { CounterProvider } from "../../../../core/platform/services/counter/provider"; -import { - ChannelCounterEntity, - ChannelUserCounterType, - TYPE as ChannelCounterEntityType, -} from "../../entities/channel-counters"; -import gr from "../../../global-resolver"; - -const logger = getLogger("channel.message-queue.new-channel-activity"); -export class NewChannelActivityProcessor - implements MessageQueueHandler -{ - private channelCounter: CounterProvider; - - async init() { - const channelCountersRepository = await gr.database.getRepository( - ChannelCounterEntityType, - ChannelCounterEntity, - ); - this.channelCounter = await gr.platformServices.counter.getCounter( - channelCountersRepository, - ); - return this; - } - - readonly topics = { - in: "channel:activity", - }; - - readonly options = { - unique: true, - ack: true, - }; - - readonly name = "NewChannelActivityProcessor"; - - validate(message: ChannelActivityNotification): boolean { - return !!(message && message.channel_id && message.company_id && message.workspace_id); - } - - async process(message: ChannelActivityNotification): Promise { - logger.info(`${this.name} - Processing new activity in channel ${message.channel_id}`); - - await this.channelCounter.increase( - { - id: message.channel_id, - company_id: message.company_id, - workspace_id: message.workspace_id, - counter_type: ChannelUserCounterType.MESSAGES, - }, - 1, - ); - - try { - await gr.services.channels.channels.updateLastActivity( - { - date: message.date, - channel: { - id: message.channel_id, - workspace_id: message.workspace_id, - company_id: message.company_id, - }, - message: { - date: message.date, - sender: message.sender, - sender_name: message.sender_name, - title: message.title, - text: message.body, - }, - }, - { - workspace: { - workspace_id: message.workspace_id, - company_id: message.company_id, - }, - user: { - id: message.sender, - }, - }, - ); - } catch (err) { - logger.error( - { err }, - `${this.name} - Error while applying channel new activity in channel ${message?.channel_id}`, - ); - } - } -} diff --git a/tdrive/backend/node/src/services/channels/services/pubsub/new-pending-emails-in-workspace-join-channels.ts b/tdrive/backend/node/src/services/channels/services/pubsub/new-pending-emails-in-workspace-join-channels.ts deleted file mode 100644 index 95c1a278..00000000 --- a/tdrive/backend/node/src/services/channels/services/pubsub/new-pending-emails-in-workspace-join-channels.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { getLogger } from "../../../../core/platform/framework"; -import { MessageQueueHandler } from "../../../../core/platform/services/message-queue/api"; -import { NewUserInWorkspaceNotification } from "../channel/types"; -import gr from "../../../global-resolver"; -import { ExecutionContext } from "../../../../core/platform/framework/api/crud-service"; - -const NAME = "Channel::NewPendingEmailsInWorkspaceJoinChannelsProcessor"; -const logger = getLogger( - "channel.message-queue.new-pending-emails-in-workspace-join-channels-processor", -); - -/** - * When a new pending email is added in a workspace, a `workspace:email:added` event is published. - * In such case, the email must be added to all the channels that he is invited. - */ -export class NewPendingEmailsInWorkspaceJoinChannelsProcessor - implements MessageQueueHandler -{ - readonly topics = { - in: "workspace:member:added", - }; - - readonly options = { - unique: true, - ack: true, - queue: "workspace:user:added:consumer_channel_pending_emails", - }; - - readonly name = NAME; - - validate(message: NewUserInWorkspaceNotification): boolean { - return !!(message && message.company_id && message.workspace_id && message.user_id); - } - - async process( - message: NewUserInWorkspaceNotification, - context?: ExecutionContext, - ): Promise { - logger.debug("Processing notification for message %o", message); - - try { - await gr.services.channelPendingEmail.proccessPendingEmails( - message, - { - workspace_id: message.workspace_id, - company_id: message.company_id, - }, - context, - ); - } catch (err) { - logger.error( - { err }, - `${this.name} - Error while processing message for pending emails %o`, - message, - ); - } - } -} diff --git a/tdrive/backend/node/src/services/channels/services/pubsub/new-user-in-workspace-join-default-channels.ts b/tdrive/backend/node/src/services/channels/services/pubsub/new-user-in-workspace-join-default-channels.ts deleted file mode 100644 index e3f682c2..00000000 --- a/tdrive/backend/node/src/services/channels/services/pubsub/new-user-in-workspace-join-default-channels.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { getLogger } from "../../../../core/platform/framework"; -import { MessageQueueHandler } from "../../../../core/platform/services/message-queue/api"; -import { NewUserInWorkspaceNotification } from "../channel/types"; -import gr from "../../../global-resolver"; - -const NAME = "Channel::NewUserInWorkspaceJoinDefaultChannelsProcessor"; -const logger = getLogger( - "channel.message-queue.new-user-in-workspace-join-default-channels-processor", -); - -/** - * When a new user is added in a workspace, a `workspace:user:added` event is published. - * In such case, the user must be added to all the default channels of the workspace. - */ -export class NewUserInWorkspaceJoinDefaultChannelsProcessor - implements MessageQueueHandler -{ - readonly topics = { - in: "workspace:member:added", - }; - - readonly options = { - unique: true, - ack: true, - queue: "workspace:user:added:consumer_default_channels", - }; - - readonly name = NAME; - - validate(message: NewUserInWorkspaceNotification): boolean { - return !!(message && message.company_id && message.workspace_id && message.user_id); - } - - async process(message: NewUserInWorkspaceNotification): Promise { - logger.debug("Processing notification for message %o", message); - - try { - const channelMembers = await gr.services.channels.channels.addUserToDefaultChannels( - { id: message.user_id }, - { - company_id: message.company_id, - workspace_id: message.workspace_id, - }, - ); - - logger.debug( - "User %s has been added as member to default channels %o", - message.user_id, - (channelMembers || []).map(c => c.channel_id), - ); - } catch (err) { - logger.error( - { err }, - `${this.name} - Error while processing message for default channels %o`, - message, - ); - } - } -} diff --git a/tdrive/backend/node/src/services/channels/services/pubsub/new-workspace.ts b/tdrive/backend/node/src/services/channels/services/pubsub/new-workspace.ts deleted file mode 100644 index 2803d49b..00000000 --- a/tdrive/backend/node/src/services/channels/services/pubsub/new-workspace.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { getLogger } from "../../../../core/platform/framework"; -import { MessageQueueHandler } from "../../../../core/platform/services/message-queue/api"; -import { ChannelActivityNotification, ChannelVisibility } from "../../types"; -import { getInstance } from "../../../channels/entities/channel"; -import gr from "../../../global-resolver"; - -const logger = getLogger("channel.message-queue.new-channel-activity"); -export class NewWorkspaceProcessor - implements MessageQueueHandler -{ - readonly topics = { - in: "workspace:added", - queue: "workspace:added:consumer1", - }; - - readonly options = { - unique: true, - ack: true, - }; - - readonly name = "NewWorkspaceProcessor"; - - validate(message: ChannelActivityNotification): boolean { - return !!(message && message.company_id && message.workspace_id); - } - - async process(message: ChannelActivityNotification): Promise { - logger.info(`${this.name} - Processing new workspace created ${message.workspace_id}`); - - try { - await gr.services.channels.channels.save( - getInstance({ - icon: "💬", - name: "General", - description: "", - visibility: ChannelVisibility.PUBLIC, - is_default: true, - }), - {}, - { - workspace: { - workspace_id: message.workspace_id, - company_id: message.company_id, - }, - user: { - id: message.sender, - }, - }, - ); - } catch (err) { - logger.error( - { err }, - `${this.name} - Error while generating default channels for workspace ${message?.workspace_id}`, - ); - } - } -} diff --git a/tdrive/backend/node/src/services/channels/services/tab.ts b/tdrive/backend/node/src/services/channels/services/tab.ts deleted file mode 100644 index a6d124ee..00000000 --- a/tdrive/backend/node/src/services/channels/services/tab.ts +++ /dev/null @@ -1,136 +0,0 @@ -import _ from "lodash"; -import { RealtimeDeleted, RealtimeSaved } from "../../../core/platform/framework"; -import { - DeleteResult, - ExecutionContext, - ListResult, - OperationType, - Pagination, - SaveResult, - UpdateResult, -} from "../../../core/platform/framework/api/crud-service"; -import { ChannelTab, ChannelTabPrimaryKey } from "../entities"; -import { ChannelExecutionContext } from "../types"; -import { Channel } from "../../../utils/types"; -import { ResourcePath } from "../../../core/platform/services/realtime/types"; -import Repository from "../../../core/platform/services/database/services/orm/repository/repository"; -import gr from "../../global-resolver"; - -export class TabServiceImpl { - version: "1"; - repository: Repository; - - async init(): Promise { - this.repository = await gr.database.getRepository("channel_tabs", ChannelTab); - return this; - } - - @RealtimeSaved((tab, context) => [ - { - room: ResourcePath.get(getTabsRealtimeRoom((context as ChannelExecutionContext).channel)), - path: getTabsRealtimeResourcePath(tab, (context as ChannelExecutionContext).channel), - }, - ]) - async save( - tab: ChannelTab, - options: SaveOptions, - context: ChannelExecutionContext, - ): Promise> { - const pk = { - company_id: context.channel.company_id, - workspace_id: context.channel.workspace_id, - channel_id: context.channel.id, - id: tab.id, - }; - - let tabEntity = await this.repository.findOne(pk, {}, context); - if (!tabEntity || !tab.id) { - tabEntity = new ChannelTab(); - tabEntity = _.merge(tabEntity, pk); - tabEntity.owner = context.user.id; - } - - tabEntity = _.merge( - tabEntity, - _.pick(tab, ["name", "configuration", "application_id", "order"]), - ); - - await this.repository.save(tabEntity, context); - - return new SaveResult("channel_tabs", tabEntity, OperationType.CREATE); - } - - async get(tabPk: ChannelTabPrimaryKey, context: ChannelExecutionContext): Promise { - const pk = { - company_id: context.channel.company_id, - workspace_id: context.channel.workspace_id, - channel_id: context.channel.id, - id: tabPk.id, - }; - return await this.repository.findOne(pk, {}, context); - } - - @RealtimeDeleted((tab, context) => [ - { - room: ResourcePath.get(getTabsRealtimeRoom((context as ChannelExecutionContext).channel)), - path: getTabsRealtimeResourcePath(tab, (context as ChannelExecutionContext).channel), - }, - ]) - async delete( - pk: ChannelTabPrimaryKey, - context?: ExecutionContext, - ): Promise> { - const tabEntity = await this.repository.findOne(pk, {}, context); - if (tabEntity) { - await this.repository.remove(tabEntity, context); - } - - return new DeleteResult("channel_tabs", tabEntity, true); - } - - async list( - pagination: Pagination, - options: ListOptions, - context: ChannelExecutionContext, - ): Promise> { - const pk = { - company_id: context.channel.company_id, - workspace_id: context.channel.workspace_id, - channel_id: context.channel.id, - }; - return await this.repository.find(pk, { pagination }, context); - } - - onUpdated( - channel: Channel, - tab: ChannelTab, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - updateResult: UpdateResult, - ): void { - console.log("Tab updated", tab); - } - - onCreated( - channel: Channel, - tab: ChannelTab, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - createResult: SaveResult, - ): void { - console.log("Tab created", tab); - } - - onDeleted(channel: Channel, tab: ChannelTab): void { - console.log("Tab deleted", tab); - } -} - -export function getTabsRealtimeRoom(channel: ChannelExecutionContext["channel"]): string { - return `/companies/${channel.company_id}/workspaces/${channel.workspace_id}/channels/${channel.id}/tabs`; -} - -export function getTabsRealtimeResourcePath( - tab: ChannelTab, - channel: ChannelExecutionContext["channel"], -): string { - return `/companies/${channel.company_id}/workspaces/${channel.workspace_id}/channels/${channel.id}/tabs/${tab.id}`; -} diff --git a/tdrive/backend/node/src/services/channels/types.ts b/tdrive/backend/node/src/services/channels/types.ts deleted file mode 100644 index 20894579..00000000 --- a/tdrive/backend/node/src/services/channels/types.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ExecutionContext } from "../../core/platform/framework/api/crud-service"; -import { Channel, uuid, Workspace } from "../../utils/types"; - -export interface WorkspaceExecutionContext extends ExecutionContext { - workspace: Workspace; -} - -export interface ChannelExecutionContext extends ExecutionContext { - channel: Channel; -} - -export interface ChannelSystemExecutionContext { - workspace: Workspace; - channel?: Channel; -} - -export enum ChannelVisibility { - PRIVATE = "private", - PUBLIC = "public", - DIRECT = "direct", -} - -export enum ChannelType { - DIRECT = "direct", -} - -export enum ChannelMemberType { - MEMBER = "member", - GUEST = "guest", - BOT = "bot", -} - -export enum ChannelMemberNotificationLevel { - // be notified on all messages - ALL = "all", - // Only be notified on @user, @all, @here, @everyone mentions - MENTIONS = "mentions", - // Only be notified on @user mention - ME = "me", - // do not be notified at all even when someone mention user, not on direct channels - NONE = "none", -} - -export type ChannelActivityNotification = { - company_id: uuid; - workspace_id: uuid | "direct"; - channel_id: uuid; - date: number; - sender: string; - title: string; - text: string; - sender_name: string; - body: string; -}; diff --git a/tdrive/backend/node/src/services/channels/utils.ts b/tdrive/backend/node/src/services/channels/utils.ts deleted file mode 100644 index caafd8a2..00000000 --- a/tdrive/backend/node/src/services/channels/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ChannelType } from "./types"; - -export function isDirectChannel(channel: { workspace_id: string }): boolean { - return channel?.workspace_id === ChannelType.DIRECT; -} diff --git a/tdrive/backend/node/src/services/channels/web/controllers/channel.ts b/tdrive/backend/node/src/services/channels/web/controllers/channel.ts deleted file mode 100644 index 43091f79..00000000 --- a/tdrive/backend/node/src/services/channels/web/controllers/channel.ts +++ /dev/null @@ -1,623 +0,0 @@ -import { plainToClass } from "class-transformer"; -import { FastifyReply, FastifyRequest } from "fastify"; -import { - CrudException, - ExecutionContext, - ListResult, - Pagination, -} from "../../../../core/platform/framework/api/crud-service"; -import { CrudController } from "../../../../core/platform/services/webserver/types"; -import { - Channel, - ChannelMember, - ChannelPendingEmails, - ChannelPendingEmailsPrimaryKey, - ChannelPrimaryKey, - getChannelPendingEmailsInstance, - UserChannel, - UsersIncludedChannel, -} from "../../entities"; -import { getWebsocketInformation, getWorkspaceRooms } from "../../services/channel/realtime"; -import { - BaseChannelsParameters, - ChannelListQueryParameters, - ChannelParameters, - ChannelPendingEmailsDeleteQueryParameters, - ChannelSaveOptions, - ChannelSearchQueryParameters, - CreateChannelBody, - ReadChannelBody, - UpdateChannelBody, -} from "../types"; -import { ChannelExecutionContext, ChannelVisibility, WorkspaceExecutionContext } from "../../types"; -import { handleError } from "../../../../utils/handleError"; -import { - ResourceCreateResponse, - ResourceDeleteResponse, - ResourceGetResponse, - ResourceListResponse, - ResourceUpdateResponse, -} from "../../../../utils/types"; -import { getLogger } from "../../../../core/platform/framework/logger"; -import _ from "lodash"; -import { ChannelMemberObject, ChannelObject } from "../../services/channel/types"; -import gr from "../../../global-resolver"; -import { checkUserBelongsToCompany } from "../../../../utils/company"; - -const logger = getLogger("channel.controller"); - -export class ChannelCrudController - implements - CrudController< - ResourceGetResponse, - ResourceCreateResponse, - ResourceListResponse, - ResourceDeleteResponse - > -{ - getPrimaryKey(request: FastifyRequest<{ Params: ChannelParameters }>): ChannelPrimaryKey { - return { - id: request.params.id, - company_id: request.params.company_id, - workspace_id: request.params.workspace_id, - }; - } - - async get( - request: FastifyRequest<{ Querystring: ChannelListQueryParameters; Params: ChannelParameters }>, - _reply: FastifyReply, - ): Promise> { - const context = getExecutionContext(request); - - let channel: Channel | UsersIncludedChannel = await gr.services.channels.channels.get( - this.getPrimaryKey(request), - context, - ); - - if (!channel) { - throw CrudException.notFound(`Channel ${request.params.id} not found`); - } - - if (Channel.isDirectChannel(channel) || Channel.isPrivateChannel(channel)) { - const isMember = await gr.services.channels.members.getChannelMember( - request.currentUser, - channel, - undefined, - context, - ); - - if (!isMember && !context.user.application_id && !context.user.server_request) { - throw CrudException.badRequest("User does not have enough rights to get channel"); - } - } - - if (request.query.include_users) - channel = await gr.services.channels.channels.includeUsersInDirectChannel(channel); - - const member = await gr.services.channels.members.get( - _.assign(new ChannelMember(), { - channel_id: channel.id, - workspace_id: channel.workspace_id, - company_id: channel.company_id, - user_id: context.user.id, - }), - context, - ); - - const channelObject = ChannelObject.mapTo(channel, { - user_member: ChannelMemberObject.mapTo(member), - }); - - await gr.services.channels.channels.completeWithStatistics([channelObject]); - - return { - websocket: gr.platformServices.realtime.sign( - [getWebsocketInformation(channel)], - context.user.id, - )[0], - resource: channelObject, - }; - } - - async search( - request: FastifyRequest<{ - Querystring: ChannelSearchQueryParameters; - Params: { company_id: string }; - }>, - _reply: FastifyReply, - ): Promise> { - if (request.query?.q?.length === 0) { - return this.recent(request); - } - - const context = getSimpleExecutionContext(request); - const userId = request.currentUser.id; - - await checkUserBelongsToCompany(request.currentUser.id, request.params.company_id); - - const limit = request.query.limit || 100; - - async function* getNextChannels(): AsyncIterableIterator { - let lastPageToken = null; - let channels: Channel[] = []; - let hasMore = true; - do { - channels = await gr.services.channels.channels - .search( - new Pagination(lastPageToken, limit.toString()), - { - search: request.query.q, - companyId: request.params.company_id, - }, - context, - ) - .then((a: ListResult) => { - lastPageToken = a.nextPage.page_token; - if (!lastPageToken) { - hasMore = false; - } - return a.getEntities(); - }); - - if (channels.length) { - for (const channel of channels) { - yield channel; - } - } else { - hasMore = false; - } - } while (hasMore); - } - - const channels = [] as (Channel & { user_member: ChannelMemberObject })[]; - - for await (const ch of getNextChannels()) { - const channelMember = await gr.services.channels.members.getChannelMember( - { id: request.currentUser.id }, - ch, - 50, - context, - ); - - //If not public channel and not member then ignore - if (!channelMember && ch.visibility !== "public") continue; - - //If public channel but not in the workspace then ignore - if ( - !channelMember && - !(await gr.services.workspaces.getUser({ workspaceId: ch.workspace_id, userId })) - ) - continue; - - const chWithUser = await gr.services.channels.channels.includeUsersInDirectChannel( - ch, - userId, - ); - - channels.push({ ...chWithUser, user_member: channelMember }); - if (channels.length == limit) { - break; - } - } - - await gr.services.channels.channels.completeWithStatistics(channels as ChannelObject[]); - - return { resources: channels }; - } - - async getForPHP( - request: FastifyRequest<{ Params: ChannelParameters }>, - reply: FastifyReply, - ): Promise { - const context = getExecutionContext(request); - const channel = await gr.services.channels.channels.get(this.getPrimaryKey(request), context); - - reply.send(channel); - } - - async thumbnail( - request: FastifyRequest<{ Params: ChannelParameters }>, - response: FastifyReply, - ): Promise { - const data = await gr.services.channels.channels.thumbnail(request.params.id); - const filename = "thumbnail.jpg"; - - response.header("Content-disposition", `inline; filename="${filename}"`); - response.type("image/jpeg"); - - response.send(data.file); - } - - async save( - request: FastifyRequest<{ - Body: CreateChannelBody; - Params: ChannelParameters; - Querystring: { include_users: boolean }; - }>, - reply: FastifyReply, - ): Promise> { - const entity = plainToClass(Channel, { - ...request.body.resource, - ...{ - company_id: request.params.company_id, - workspace_id: request.params.workspace_id, - }, - }); - logger.debug("reqId: %s - save - Channel input %o", request.id, entity); - - try { - const options = { - members: request.body.options ? request.body.options.members || [] : [], - addCreatorAsMember: true, - } as ChannelSaveOptions; - - const context = getExecutionContext(request); - const channelResult = await gr.services.channels.channels.save(entity, options, context); - - logger.debug("reqId: %s - save - Channel %s created", request.id, channelResult.entity.id); - - const member = await gr.services.channels.members.get( - _.assign(new ChannelMember(), { - channel_id: channelResult.entity.id, - workspace_id: channelResult.entity.workspace_id, - company_id: channelResult.entity.company_id, - user_id: context.user.id, - }), - context, - ); - - let entityWithUsers: Channel = channelResult.entity; - - if (request.query.include_users) - entityWithUsers = await gr.services.channels.channels.includeUsersInDirectChannel( - entityWithUsers, - context.user.id, - ); - - const resultEntity = { - ...entityWithUsers, - ...{ user_member: member }, - } as unknown as UserChannel; - - if (entity) { - reply.code(201); - } - - return { - websocket: gr.platformServices.realtime.sign( - [getWebsocketInformation(channelResult.entity)], - context.user.id, - )[0], - resource: ChannelObject.mapTo(resultEntity), - }; - } catch (err) { - handleError(reply, err); - } - } - - async update( - request: FastifyRequest<{ Body: UpdateChannelBody; Params: ChannelParameters }>, - reply: FastifyReply, - ): Promise> { - const entity = plainToClass(Channel, { - ...request.body.resource, - ...{ - company_id: request.params.company_id, - workspace_id: request.params.workspace_id, - id: request.params.id, - }, - }); - - try { - const context = getExecutionContext(request); - const result = await gr.services.channels.channels.save(entity, {}, context); - - if (result.entity) { - reply.code(201); - } - - return { - websocket: gr.platformServices.realtime.sign( - [getWebsocketInformation(result.entity)], - context.user.id, - )[0], - resource: ChannelObject.mapTo(result.entity), - }; - } catch (err) { - handleError(reply, err); - } - } - - async list( - request: FastifyRequest<{ - Querystring: ChannelListQueryParameters; - Params: BaseChannelsParameters; - }>, - ): Promise> { - const context = getExecutionContext(request); - - //Fixme: this slow down the channel get operation. Once we stabilize the workspace:member:added event we can remove this - if (context.workspace.workspace_id !== ChannelVisibility.DIRECT) { - await gr.services.channelPendingEmail.proccessPendingEmails( - { - user_id: context.user.id, - ...context.workspace, - }, - context.workspace, - context, - ); - } - - const list = await gr.services.channels.channels.list( - new Pagination(request.query.page_token, request.query.limit), - { ...request.query }, - context, - ); - - let entities = []; - if (request.query.include_users) { - entities = []; - for (const e of list.getEntities()) { - entities.push( - await gr.services.channels.channels.includeUsersInDirectChannel(e, context.user.id), - ); - } - } else { - entities = list.getEntities(); - } - - const resources = entities.map(a => ChannelObject.mapTo(a)); - - await gr.services.channels.channels.completeWithStatistics(resources); - - return { - ...{ - resources: resources, - }, - ...(request.query.websockets && { - websockets: gr.platformServices.realtime.sign( - getWorkspaceRooms(request.params, request.currentUser), - request.currentUser.id, - ), - }), - ...(list.page_token && { - next_page_token: list.page_token, - }), - }; - } - - async delete( - request: FastifyRequest<{ Params: ChannelParameters }>, - reply: FastifyReply, - ): Promise { - try { - const deleteResult = await gr.services.channels.channels.delete( - this.getPrimaryKey(request), - getExecutionContext(request), - ); - - if (deleteResult.deleted) { - reply.code(204); - - return { - status: "success", - }; - } - - return { - status: "error", - }; - } catch (err) { - handleError(reply, err); - } - } - - async updateRead( - request: FastifyRequest<{ Body: ReadChannelBody; Params: ChannelParameters }>, - reply: FastifyReply, - ): Promise { - const read = request.body.value; - const context = getExecutionContext(request); - try { - const result = read - ? await gr.services.channels.channels.markAsRead( - this.getPrimaryKey(request), - request.currentUser, - context, - ) - : await gr.services.channels.channels.markAsUnread( - this.getPrimaryKey(request), - request.currentUser, - context, - ); - return result; - } catch (err) { - handleError(reply, err); - } - } - - async findPendingEmails( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - request: FastifyRequest<{ - Querystring: ChannelListQueryParameters; - Params: ChannelPendingEmailsPrimaryKey; - }>, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - reply: FastifyReply, - ): Promise> { - const list = await gr.services.channelPendingEmail.list( - new Pagination(request.query.page_token, request.query.limit), - { ...request.query }, - getChannelPendingEmailsExecutionContext(request), - ); - - return { - resources: list.getEntities(), - ...(request.query.websockets && { - websockets: gr.platformServices.realtime.sign( - getWorkspaceRooms(request.params, request.currentUser), - request.currentUser.id, - ), - }), - ...(list.page_token && { - next_page_token: list.page_token, - }), - }; - } - - async savePendingEmails( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - request: FastifyRequest<{ - Body: { resource: ChannelPendingEmails }; - Params: ChannelPendingEmailsPrimaryKey; - }>, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - reply: FastifyReply, - ): Promise> { - const context = getSimpleExecutionContext(request); - const pendingEmail = await gr.services.channelPendingEmail.create( - getChannelPendingEmailsInstance(request.body.resource), - context, - ); - logger.debug("reqId: %s - save - PendingEmails input %o", request.id, pendingEmail.entity); - return { resource: pendingEmail.entity }; - } - - async deletePendingEmails( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - request: FastifyRequest<{ Params: ChannelPendingEmailsDeleteQueryParameters }>, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - reply: FastifyReply, - ): Promise { - const pendingEmail = await gr.services.channelPendingEmail.delete( - getChannelPendingEmailsInstance({ - channel_id: request.params.channel_id, - company_id: request.params.company_id, - workspace_id: request.params.workspace_id, - email: request.params.email, - }), - ); - - return { status: pendingEmail.deleted ? "success" : "error" }; - } - - async recent( - request: FastifyRequest<{ - Querystring: { limit?: string }; - Params: Pick; - }>, - ): Promise> { - const companyId = request.params.company_id; - const userId = request.currentUser.id; - - const context = getSimpleExecutionContext(request); - const limit = parseInt(request.query.limit) || 100; - - const workspaces = ( - await gr.services.workspaces.getAllForUser({ userId }, { id: companyId }) - ).map(a => a.workspaceId); - - let channels: UserChannel[] = await gr.services.channels.channels - .getChannelsForUsersInWorkspace(companyId, "direct", userId, undefined, context) - .then(list => list.getEntities()); - - for (const workspaceId of workspaces) { - const workspaceChannels = await gr.services.channels.channels - .getChannelsForUsersInWorkspace(companyId, workspaceId, userId, undefined, context) - .then(list => list.getEntities()); - channels = [...channels, ...workspaceChannels]; - } - - channels = channels.sort( - (a, b) => - (b.last_activity || 0) / 100 + - (b.user_member.last_access || 0) - - ((a.user_member.last_access || 0) + (a.last_activity || 0) / 100), - ); - channels = channels.slice(0, 100); - - if (channels.length < limit) { - let otherChannels: UserChannel[] = []; - for (const workspaceId of workspaces) { - //Include channels we could join in this result - otherChannels = [ - ...otherChannels, - ...( - await gr.services.channels.channels.getAllChannelsInWorkspace( - companyId, - workspaceId, - context, - ) - ).map(ch => { - return { - ...ch, - user_member: null, - } as UserChannel; - }), - ]; - } - - channels = _.unionBy(channels, otherChannels, "id"); - } - - const userIncludedChannels: UsersIncludedChannel[] = await Promise.all( - channels.map(channel => { - return gr.services.channels.channels.includeUsersInDirectChannel(channel, userId); - }), - ); - - const resources = userIncludedChannels.slice(0, limit).map(r => ChannelObject.mapTo(r, {})); - await gr.services.channels.channels.completeWithStatistics(resources); - - await new Promise(r => setTimeout(r, 2000)); - - return { - resources, - }; - } -} - -function getExecutionContext( - request: FastifyRequest<{ Params: BaseChannelsParameters }>, -): WorkspaceExecutionContext { - return { - user: request.currentUser, - url: request.url, - method: request.routerMethod, - reqId: request.id, - transport: "http", - workspace: { - company_id: request.params.company_id, - workspace_id: request.params.workspace_id, - }, - }; -} - -function getChannelPendingEmailsExecutionContext( - request: FastifyRequest<{ - Params: ChannelPendingEmailsPrimaryKey | ChannelPendingEmailsDeleteQueryParameters; - }>, -): ChannelExecutionContext { - return { - user: request.currentUser, - url: request.url, - method: request.routerMethod, - reqId: request.id, - transport: "http", - channel: { - id: request.params.channel_id, - company_id: request.params.company_id, - workspace_id: request.params.workspace_id, - }, - }; -} - -function getSimpleExecutionContext(request: FastifyRequest): ExecutionContext { - return { - user: request.currentUser, - url: request.url, - method: request.routerMethod, - reqId: request.id, - transport: "http", - }; -} diff --git a/tdrive/backend/node/src/services/channels/web/controllers/index.ts b/tdrive/backend/node/src/services/channels/web/controllers/index.ts deleted file mode 100644 index 37fb690c..00000000 --- a/tdrive/backend/node/src/services/channels/web/controllers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./channel"; -export * from "./member"; -export * from "./tab"; diff --git a/tdrive/backend/node/src/services/channels/web/controllers/member.ts b/tdrive/backend/node/src/services/channels/web/controllers/member.ts deleted file mode 100644 index 60e1cb02..00000000 --- a/tdrive/backend/node/src/services/channels/web/controllers/member.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { CrudController } from "../../../../core/platform/services/webserver/types"; -import { CrudException, Pagination } from "../../../../core/platform/framework/api/crud-service"; -import { - ChannelMember, - ChannelMemberPrimaryKey, - ChannelMemberReadCursors, - ChannelMemberWithUser, -} from "../../entities"; -import { - ChannelMemberParameters, - ChannelParameters, - CreateChannelMemberBody, - PaginationQueryParameters, - UpdateChannelMemberBody, -} from "../types"; -import { FastifyReply, FastifyRequest } from "fastify"; -import { ChannelExecutionContext } from "../../types"; -import { plainToClass } from "class-transformer"; -import { handleError } from "../../../../utils/handleError"; -import { getChannelRooms } from "../../services/member/realtime"; -import { - ResourceCreateResponse, - ResourceDeleteResponse, - ResourceGetResponse, - ResourceListResponse, - ResourceUpdateResponse, - User, -} from "../../../../utils/types"; -import gr from "../../../global-resolver"; -import { formatUser } from "../../../../utils/users"; - -export class ChannelMemberCrudController - implements - CrudController< - ResourceGetResponse, - ResourceCreateResponse, - ResourceListResponse, - ResourceDeleteResponse - > -{ - getPrimaryKey( - request: FastifyRequest<{ Params: ChannelMemberParameters }>, - ): ChannelMemberPrimaryKey { - return { - user_id: request.params.member_id, - channel_id: request.params.id, - company_id: request.params.company_id, - workspace_id: request.params.workspace_id, - }; - } - - async get( - request: FastifyRequest<{ Params: ChannelMemberParameters }>, - _reply: FastifyReply, - ): Promise> { - if (!isCurrentUser(request.params.member_id, request.currentUser)) { - throw CrudException.badRequest("User does not have enough rights to get member"); - } - const context = getExecutionContext(request); - const resource = await gr.services.channels.members.get(this.getPrimaryKey(request), context); - - if (!resource) { - throw CrudException.notFound(`Channel member ${request.params.member_id} not found`); - } - - return { - resource, - }; - } - - async save( - request: FastifyRequest<{ Body: CreateChannelMemberBody; Params: ChannelMemberParameters }>, - reply: FastifyReply, - ): Promise> { - const entity = plainToClass(ChannelMember, { - ...request.body.resource, - ...{ - company_id: request.params.company_id, - workspace_id: request.params.workspace_id, - channel_id: request.params.id, - }, - }); - - try { - const result = await gr.services.channels.members.save(entity, getExecutionContext(request)); - - if (result.entity) { - reply.code(201); - } - - return { - resource: result.entity, - }; - } catch (err) { - handleError(reply, err); - } - } - - async update( - request: FastifyRequest<{ Body: UpdateChannelMemberBody; Params: ChannelMemberParameters }>, - reply: FastifyReply, - ): Promise> { - const entity = plainToClass(ChannelMember, { - ...request.body.resource, - ...this.getPrimaryKey(request), - }); - - if (!isCurrentUser(entity.user_id, request.currentUser)) { - throw CrudException.badRequest("Current user can not update this member"); - } - - try { - const result = await gr.services.channels.members.save(entity, getExecutionContext(request)); - - if (result.entity) { - reply.code(200); - } - - return { - resource: result.entity, - }; - } catch (err) { - handleError(reply, err); - } - } - - async delete( - request: FastifyRequest<{ Params: ChannelMemberParameters }>, - reply: FastifyReply, - ): Promise { - try { - const deleteResult = await gr.services.channels.members.delete( - this.getPrimaryKey(request), - getExecutionContext(request), - ); - - if (deleteResult.deleted) { - reply.code(204); - - return { - status: "success", - }; - } - - return { - status: "error", - }; - } catch (err) { - handleError(reply, err); - } - } - - /** - * List channel members - * - * @param request - */ - async list( - request: FastifyRequest<{ - Querystring: PaginationQueryParameters & { company_role?: string; search?: string }; - Params: ChannelParameters; - }>, - ): Promise> { - let list: ChannelMember[] = []; - let nextPageToken: string = null; - const resources = []; - const context = getExecutionContext(request); - - if (request.query.search) { - const users = await gr.services.users.search( - new Pagination(request.query.page_token, request.query.limit), - { - search: request.query.search, - companyId: request.params.company_id, - }, - context, - ); - - nextPageToken = users.nextPage?.page_token; - - for (const user of users.getEntities()) { - const channelMember = await gr.services.channels.members.getChannelMember( - user, - { - company_id: request.params.company_id, - workspace_id: request.params.workspace_id, - id: request.params.id, - }, - undefined, - context, - ); - - if (channelMember) { - list.push(channelMember); - } - } - } else { - const channelMembers = await gr.services.channels.members.list( - new Pagination(request.query.page_token, request.query.limit), - { company_role: request.query.company_role }, - context, - ); - - nextPageToken = channelMembers.nextPage?.page_token; - list = channelMembers.getEntities(); - } - - for (const member of list) { - if (member) { - const user = await formatUser(await gr.services.users.getCached({ id: member.user_id }), { - includeCompanies: true, - }); - resources.push({ ...member, user }); - } - } - - return { - ...{ - resources, - }, - ...(request.query.websockets && { - websockets: gr.platformServices.realtime.sign( - getChannelRooms(request.params, request.currentUser), - request.currentUser.id, - ), - }), - next_page_token: nextPageToken, - }; - } - - /** - * - * @param request Private exists - * @param reply - */ - async exists( - request: FastifyRequest<{ Params: ChannelMemberParameters }>, - reply: FastifyReply, - ): Promise { - const context = getExecutionContext(request); - const resource = await gr.services.channels.members.get(this.getPrimaryKey(request), context); - - if (!resource) { - return reply.status(200).send({ has_access: false }); - } - - return reply.status(200).send({ has_access: true }); - } - - /** - * Lists the channel read sections for all members. - * - * @param {FastifyRequest<{ Params: ChannelParameters }>} request - the request object - * @param {FastifyReply} reply - the reply object - * @returns {Promise>} - the response - */ - async getAllChannelMembersReadSections( - request: FastifyRequest<{ Params: ChannelParameters }>, - reply: FastifyReply, - ): Promise> { - try { - const context = getExecutionContext(request); - const channelMembersReadSections = - await gr.services.channels.members.getChannelMembersReadSections(context); - - return { - resources: channelMembersReadSections.getEntities(), - }; - } catch (err) { - handleError(reply, err); - } - } - - /** - * Get the channel member read section for a user. - * - * @param {FastifyRequest<{ Params: ChannelMemberParameters }>} request - the request object - * @param {FastifyReply} reply - the reply object - * @returns {Promise>} - the read section for the member - */ - async getChannelMemberReadSections( - request: FastifyRequest<{ Params: ChannelMemberParameters }>, - reply: FastifyReply, - ): Promise> { - try { - const { member_id } = request.params; - const context = getExecutionContext(request); - const channelMembersReadSections = - await gr.services.channels.members.getChannelMemberReadSections(member_id, context); - - return { - resource: channelMembersReadSections, - }; - } catch (err) { - handleError(reply, err); - } - } -} - -function getExecutionContext( - request: FastifyRequest<{ Params: ChannelParameters }>, -): ChannelExecutionContext { - return { - user: request.currentUser, - url: request.url, - method: request.routerMethod, - reqId: request.id, - transport: "http", - channel: { - id: request.params.id, - company_id: request.params.company_id, - workspace_id: request.params.workspace_id, - }, - }; -} - -function isCurrentUser(memberId: string, user: User): boolean { - return memberId && memberId === user.id; -} diff --git a/tdrive/backend/node/src/services/channels/web/controllers/tab.ts b/tdrive/backend/node/src/services/channels/web/controllers/tab.ts deleted file mode 100644 index 9383f6d6..00000000 --- a/tdrive/backend/node/src/services/channels/web/controllers/tab.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { CrudController } from "../../../../core/platform/services/webserver/types"; -import { CrudException, Pagination } from "../../../../core/platform/framework/api/crud-service"; -import { ChannelTab, ChannelTabPrimaryKey } from "../../entities"; -import { - ChannelParameters, - ChannelTabParameters, - CreateChannelTabBody, - PaginationQueryParameters, - UpdateChannelTabBody, -} from "../types"; -import { FastifyReply, FastifyRequest } from "fastify"; -import { ChannelExecutionContext } from "../../types"; -import { plainToClass } from "class-transformer"; -import { handleError } from "../../../../utils/handleError"; -import { - ResourceCreateResponse, - ResourceDeleteResponse, - ResourceEventsPayload, - ResourceGetResponse, - ResourceListResponse, - ResourceUpdateResponse, -} from "../../../../utils/types"; -import { localEventBus } from "../../../../core/platform/framework/event-bus"; -import { getTabsRealtimeRoom } from "../../services/tab"; -import gr from "../../../global-resolver"; - -export class ChannelTabCrudController - implements - CrudController< - ResourceGetResponse, - ResourceCreateResponse, - ResourceListResponse, - ResourceDeleteResponse - > -{ - getPrimaryKey(request: FastifyRequest<{ Params: ChannelTabParameters }>): ChannelTabPrimaryKey { - return { - id: request.params.tab_id, - channel_id: request.params.id, - company_id: request.params.company_id, - workspace_id: request.params.workspace_id, - }; - } - - async get( - request: FastifyRequest<{ Params: ChannelTabParameters }>, - _reply: FastifyReply, - ): Promise> { - const resource = await gr.services.tab.get( - this.getPrimaryKey(request), - getExecutionContext(request), - ); - - if (!resource) { - throw CrudException.notFound(`Channel member ${request.params.tab_id} not found`); - } - - return { - resource, - }; - } - - async save( - request: FastifyRequest<{ Body: CreateChannelTabBody; Params: ChannelTabParameters }>, - reply: FastifyReply, - ): Promise> { - const entity = plainToClass(ChannelTab, { - ...request.body.resource, - ...{ - company_id: request.params.company_id, - workspace_id: request.params.workspace_id, - channel_id: request.params.id, - }, - }); - - try { - const context = getExecutionContext(request); - const result = await gr.services.tab.save(entity, {}, context); - - if (result.entity) { - localEventBus.publish("channel:tab:created", { - tab: result.entity, - actor: getExecutionContext(request).user, - resourcesAfter: [result.entity], - channelParameters: request.params, - user: context.user, - }); - reply.code(201); - } - - return { - resource: result.entity, - }; - } catch (err) { - handleError(reply, err); - } - } - - async update( - request: FastifyRequest<{ Body: UpdateChannelTabBody; Params: ChannelTabParameters }>, - reply: FastifyReply, - ): Promise> { - const entity = plainToClass(ChannelTab, { - ...request.body.resource, - ...this.getPrimaryKey(request), - }); - - try { - const result = await gr.services.tab.save(entity, {}, getExecutionContext(request)); - - if (result.entity) { - reply.code(200); - } - - return { - resource: result.entity, - }; - } catch (err) { - handleError(reply, err); - } - } - - async delete( - request: FastifyRequest<{ Params: ChannelTabParameters }>, - reply: FastifyReply, - ): Promise { - try { - const context = getExecutionContext(request); - const deleteResult = await gr.services.tab.delete(this.getPrimaryKey(request)); - - if (deleteResult.deleted) { - localEventBus.publish("channel:tab:deleted", { - tab: deleteResult.entity, - actor: getExecutionContext(request).user, - resourcesAfter: [deleteResult.entity], - channelParameters: request.params, - user: context.user, - }); - reply.code(204); - - return { - status: "success", - }; - } - - return { - status: "error", - }; - } catch (err) { - handleError(reply, err); - } - } - - /** - * List channel tabs - * - * @param request - */ - async list( - request: FastifyRequest<{ - Querystring: PaginationQueryParameters; - Params: ChannelParameters; - }>, - ): Promise> { - const context = getExecutionContext(request); - const list = await gr.services.tab.list( - new Pagination(request.query.page_token, request.query.limit), - {}, - context, - ); - - return { - ...{ - resources: list.getEntities(), - }, - ...(request.query.websockets && { - websockets: gr.platformServices.realtime.sign( - [{ room: getTabsRealtimeRoom(context.channel) }], - context.user.id, - ), - }), - ...(list.page_token && { - next_page_token: list.page_token, - }), - }; - } -} - -function getExecutionContext( - request: FastifyRequest<{ Params: ChannelParameters }>, -): ChannelExecutionContext { - return { - user: request.currentUser, - url: request.url, - method: request.routerMethod, - reqId: request.id, - transport: "http", - channel: { - id: request.params.id, - company_id: request.params.company_id, - workspace_id: request.params.workspace_id, - }, - }; -} diff --git a/tdrive/backend/node/src/services/channels/web/index.ts b/tdrive/backend/node/src/services/channels/web/index.ts deleted file mode 100644 index 2945a147..00000000 --- a/tdrive/backend/node/src/services/channels/web/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { FastifyInstance, FastifyRegisterOptions } from "fastify"; -import routes from "./routes"; - -export default ( - fastify: FastifyInstance, - options: FastifyRegisterOptions<{ - prefix: string; - }>, -): void => { - fastify.log.debug("Configuring /internal/services/channels/v1 routes"); - fastify.register(routes, options); -}; diff --git a/tdrive/backend/node/src/services/channels/web/middleware.ts b/tdrive/backend/node/src/services/channels/web/middleware.ts deleted file mode 100644 index c0e77242..00000000 --- a/tdrive/backend/node/src/services/channels/web/middleware.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { validate as validateUuid } from "uuid"; -import { ChannelVisibility } from "../types"; - -export function checkCompanyAndWorkspaceForUser( - companyId: string, - workspaceId: string, -): Promise { - return Promise.resolve( - validateUuid(companyId) && - (validateUuid(workspaceId) || workspaceId === ChannelVisibility.DIRECT), - ); -} diff --git a/tdrive/backend/node/src/services/channels/web/routes.ts b/tdrive/backend/node/src/services/channels/web/routes.ts deleted file mode 100644 index 3e40beb7..00000000 --- a/tdrive/backend/node/src/services/channels/web/routes.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { FastifyInstance, FastifyPluginCallback } from "fastify"; -import { - BaseChannelsParameters, - ChannelParameters, - PaginationQueryParameters, - RecentChannelsParameters, -} from "./types"; -import { - createChannelMemberSchema, - createChannelSchema, - getChannelMemberSchema, - getChannelSchema, - updateChannelMemberSchema, - updateChannelSchema, -} from "./schemas"; -import { - ChannelCrudController, - ChannelMemberCrudController, - ChannelTabCrudController, -} from "./controllers"; -import { checkCompanyAndWorkspaceForUser } from "./middleware"; -import { FastifyRequest } from "fastify/types/request"; -import { checkUserBelongsToCompany } from "../../../utils/company"; - -const channelsUrl = "/companies/:company_id/workspaces/:workspace_id/channels"; -const membersUrl = `${channelsUrl}/:id/members`; -const tabsUrl = `${channelsUrl}/:id/tabs`; -const pendingEmailsUrl = `${channelsUrl}/:channel_id/pending_emails`; - -const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next) => { - const channelsController = new ChannelCrudController(); - const membersController = new ChannelMemberCrudController(); - const tabsController = new ChannelTabCrudController(); - - const accessControlCompanyOnly = async ( - request: FastifyRequest<{ Params: RecentChannelsParameters }>, - ) => { - await checkUserBelongsToCompany(request.currentUser.id, request.params.company_id); - }; - - const accessControl = async (request: FastifyRequest<{ Params: BaseChannelsParameters }>) => { - const authorized = await checkCompanyAndWorkspaceForUser( - request.params.company_id, - request.params.workspace_id, - ); - - if (!authorized) { - throw fastify.httpErrors.badRequest("Invalid company/workspace"); - } - }; - - const validateQuery = async ( - request: FastifyRequest<{ - Params: BaseChannelsParameters; - Querystring: PaginationQueryParameters; - }>, - ) => { - if (isNaN(+request.query.limit)) { - request.query.limit = "100"; - } - }; - - // channels - fastify.route({ - method: "GET", - url: channelsUrl, - preHandler: [accessControl, validateQuery], - preValidation: [fastify.authenticate], - handler: channelsController.list.bind(channelsController), - }); - - fastify.route({ - method: "GET", - url: `${channelsUrl}/:id`, - preHandler: accessControl, - preValidation: [fastify.authenticate], - schema: getChannelSchema, - handler: channelsController.get.bind(channelsController), - }); - - fastify.route({ - method: "GET", - url: `${channelsUrl}/:id/thumbnail`, - handler: channelsController.thumbnail.bind(channelsController), - }); - - fastify.route({ - method: "POST", - url: channelsUrl, - preHandler: accessControl, - preValidation: [fastify.authenticate], - schema: createChannelSchema, - handler: channelsController.save.bind(channelsController), - }); - - fastify.route({ - method: "POST", - url: `${channelsUrl}/:id`, - preHandler: accessControl, - preValidation: [fastify.authenticate], - schema: updateChannelSchema, - handler: channelsController.update.bind(channelsController), - }); - - fastify.route<{ Params: ChannelParameters }>({ - method: "DELETE", - url: `${channelsUrl}/:id`, - preHandler: accessControl, - preValidation: [fastify.authenticate], - handler: channelsController.delete.bind(channelsController), - }); - - fastify.route<{ Params: ChannelParameters }>({ - method: "POST", - url: `${channelsUrl}/:id/read`, - preHandler: accessControl, - preValidation: [fastify.authenticate], - handler: channelsController.updateRead.bind(channelsController), - }); - - fastify.route({ - method: "GET", - url: "/companies/:company_id/search", - preValidation: [fastify.authenticate], - handler: channelsController.search.bind(channelsController), - }); - - fastify.route({ - method: "GET", - url: "/companies/:company_id/channels/recent", - preHandler: accessControlCompanyOnly, - preValidation: [fastify.authenticate], - handler: channelsController.recent.bind(channelsController), - }); - - // members - - fastify.route({ - method: "GET", - url: membersUrl, - preHandler: accessControl, - preValidation: [fastify.authenticate], - handler: membersController.list.bind(membersController), - }); - - fastify.route({ - method: "POST", - url: membersUrl, - preHandler: accessControl, - preValidation: [fastify.authenticate], - schema: createChannelMemberSchema, - handler: membersController.save.bind(membersController), - }); - - fastify.route({ - method: "GET", - url: `${membersUrl}/:member_id`, - preHandler: accessControl, - preValidation: [fastify.authenticate], - schema: getChannelMemberSchema, - handler: membersController.get.bind(membersController), - }); - - fastify.route({ - method: "POST", - url: `${membersUrl}/:member_id`, - preHandler: accessControl, - preValidation: [fastify.authenticate], - schema: updateChannelMemberSchema, - handler: membersController.update.bind(membersController), - }); - - fastify.route({ - method: "DELETE", - url: `${membersUrl}/:member_id`, - preHandler: accessControl, - preValidation: [fastify.authenticate], - handler: membersController.delete.bind(membersController), - }); - - fastify.route({ - method: "GET", - url: `${membersUrl}/read_sections`, - preHandler: accessControl, - preValidation: [fastify.authenticate], - handler: membersController.getAllChannelMembersReadSections.bind(membersController), - }); - - fastify.route({ - method: "GET", - url: `${membersUrl}/:member_id/read_sections`, - preHandler: accessControl, - preValidation: [fastify.authenticate], - handler: membersController.getChannelMemberReadSections.bind(membersController), - }); - - // pending_emails - - fastify.route({ - method: "GET", - url: pendingEmailsUrl, - preHandler: [accessControl, validateQuery], - preValidation: [fastify.authenticate], - //schema: getChannelPendingEmailsSchema, - handler: channelsController.findPendingEmails.bind(channelsController), - }); - - fastify.route({ - method: "POST", - url: pendingEmailsUrl, - preHandler: accessControl, - preValidation: [fastify.authenticate], - //schema: createChannelPendingEmailsSchema, - handler: channelsController.savePendingEmails.bind(channelsController), - }); - - fastify.route({ - method: "DELETE", - url: `${pendingEmailsUrl}/:email`, - preHandler: accessControl, - preValidation: [fastify.authenticate], - handler: channelsController.deletePendingEmails.bind(channelsController), - }); - - // tabs - - fastify.route({ - method: "GET", - url: tabsUrl, - preHandler: accessControl, - preValidation: [fastify.authenticate], - handler: tabsController.list.bind(tabsController), - }); - - fastify.route({ - method: "POST", - url: tabsUrl, - preHandler: accessControl, - preValidation: [fastify.authenticate], - handler: tabsController.save.bind(tabsController), - }); - - fastify.route({ - method: "GET", - url: `${tabsUrl}/:tab_id`, - preHandler: accessControl, - preValidation: [fastify.authenticate], - handler: tabsController.get.bind(tabsController), - }); - - fastify.route({ - method: "POST", - url: `${tabsUrl}/:tab_id`, - preHandler: accessControl, - preValidation: [fastify.authenticate], - handler: tabsController.update.bind(tabsController), - }); - - fastify.route({ - method: "DELETE", - url: `${tabsUrl}/:tab_id`, - preHandler: accessControl, - preValidation: [fastify.authenticate], - handler: tabsController.delete.bind(tabsController), - }); - - next(); -}; - -export default routes; diff --git a/tdrive/backend/node/src/services/channels/web/schemas.ts b/tdrive/backend/node/src/services/channels/web/schemas.ts deleted file mode 100644 index 7fdc8961..00000000 --- a/tdrive/backend/node/src/services/channels/web/schemas.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { webSocketSchema } from "../../../utils/types"; - -const channelMemberSchema = { - type: "object", - properties: { - id: { type: "string" }, - user_id: { type: "string" }, - channel_id: { type: "string" }, - company_id: { type: "string" }, - workspace_id: { type: "string" }, - type: { type: "string", enum: ["member", "guest", "bot"] }, - favorite: { type: "boolean" }, - last_access: { type: "number" }, - last_increment: { type: "number" }, - notification_level: { - type: "string", - enum: ["all", "none", "group_mentions", "user_mentions"], - }, - }, -}; - -const channelSchema = { - type: "object", - properties: { - id: { type: "string" }, - company_id: { type: "string" }, - workspace_id: { type: "string" }, - type: { type: "string", enum: ["workspace", "direct"] }, - name: { type: "string" }, - description: { type: "string" }, - archivation_date: { type: "number" }, - archived: { type: "boolean" }, - channel_group: { type: "string" }, - connectors: { type: ["array", "null"] }, - icon: { type: "string" }, - default: { type: "boolean" }, - members: { type: ["array", "null"] }, - users: { type: ["array", "null"] }, - owner: { type: "string" }, - visibility: { type: "string", enum: ["private", "public", "direct"] }, - last_activity: { type: "number" }, - user_member: {}, - direct_channel_members: { type: "array" }, - stats: {}, - is_readonly: { type: "boolean" }, - }, -}; - -const channelTabSchema = { - type: "object", - properties: { - company_id: { type: "string" }, - workspace_id: { type: "string" }, - channel_id: { type: "string" }, - id: { type: "string" }, - owner: { type: "string" }, - order: { type: "string" }, - configuration: { - type: "object", - properties: { - board_id: { type: "string" }, - directory_id: { type: "string" }, - }, - }, - application_id: { type: "string" }, - name: { type: "string" }, - }, -}; - -export const createChannelSchema = { - body: { - type: "object", - properties: { - options: { - type: "object", - properties: { - members: { type: "array" }, - }, - }, - resource: { - type: "object", - }, - }, - required: ["resource"], - }, - response: { - 201: { - type: "object", - properties: { - websocket: webSocketSchema, - resource: channelSchema, - }, - required: ["resource"], - }, - }, -}; - -export const updateChannelSchema = { - body: { - type: "object", - properties: { - resource: { - type: "object", - }, - }, - required: ["resource"], - }, - response: { - 200: { - type: "object", - properties: { - websocket: webSocketSchema, - resource: channelSchema, - }, - required: ["resource"], - }, - }, -}; - -export const getChannelSchema = { - request: { - properties: { - company_id: { type: "string" }, - workspace_id: { type: "string" }, - }, - required: ["company_id", "workspace_id"], - }, - response: { - 200: { - type: "object", - properties: { - websocket: webSocketSchema, - resource: channelSchema, - }, - }, - }, -}; - -export const getChannelMemberSchema = { - response: { - 200: { - type: "object", - properties: { - websocket: webSocketSchema, - resource: channelMemberSchema, - }, - required: ["resource"], - }, - }, -}; - -export const createChannelMemberSchema = { - body: { - type: "object", - properties: { - resource: channelMemberSchema, - }, - required: ["resource"], - }, - response: { - 201: { - type: "object", - properties: { - websocket: webSocketSchema, - resource: channelMemberSchema, - }, - required: ["resource"], - }, - }, -}; - -export const updateChannelMemberSchema = { - body: { - type: "object", - properties: { - resource: { - type: "object", - properties: { - favorite: { type: "boolean" }, - notification_level: { type: "string" }, - }, - }, - }, - required: ["resource"], - }, - response: { - 200: { - type: "object", - properties: { - websocket: webSocketSchema, - resource: channelMemberSchema, - }, - required: ["resource"], - }, - }, -}; - -export const getChannelTabSchema = { - response: { - 200: { - type: "object", - properties: { - websocket: webSocketSchema, - resource: channelTabSchema, - }, - required: ["resource"], - }, - }, -}; - -export const createChannelTabSchema = { - body: { - type: "object", - properties: { - resource: { - type: "object", - properties: {}, - }, - }, - required: ["resource"], - }, - response: { - 201: { - type: "object", - properties: { - websocket: webSocketSchema, - resource: channelTabSchema, - }, - required: ["resource"], - }, - }, -}; - -export const updateChannelTabSchema = { - body: { - type: "object", - properties: { - resource: { - type: "object", - properties: {}, - }, - }, - required: ["resource"], - }, - response: { - 200: { - type: "object", - properties: { - websocket: webSocketSchema, - resource: channelTabSchema, - }, - required: ["resource"], - }, - }, -}; - -const channelPendingEmailsSchema = { - type: "object", - properties: { - company_id: { type: "string" }, - workspace_id: { type: "string" }, - channel_id: { type: "string" }, - email: { type: "string" }, - }, -}; - -export const getChannelPendingEmailsSchema = { - response: { - 200: { - type: "object", - properties: { - websocket: webSocketSchema, - resource: channelPendingEmailsSchema, - }, - required: ["resource"], - }, - }, -}; - -export const createChannelPendingEmailsSchema = { - body: { - type: "object", - properties: { - resource: { - type: "object", - properties: {}, - }, - }, - required: ["resource"], - }, - response: { - 201: { - type: "object", - properties: { - websocket: webSocketSchema, - resource: channelPendingEmailsSchema, - }, - required: ["resource"], - }, - }, -}; diff --git a/tdrive/backend/node/src/services/channels/web/types.ts b/tdrive/backend/node/src/services/channels/web/types.ts deleted file mode 100644 index dad0eabb..00000000 --- a/tdrive/backend/node/src/services/channels/web/types.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Channel, ChannelMember, ChannelPendingEmails, ChannelTab } from "../entities"; - -export declare type DirectChannel = "direct"; - -export interface RecentChannelsParameters { - company_id: string; -} - -export interface BaseChannelsParameters { - company_id: string; - workspace_id: string | DirectChannel; -} - -export interface ChannelParameters extends BaseChannelsParameters { - /** the channel id */ - id: string; -} - -export interface ChannelMemberParameters extends ChannelParameters { - member_id: string; -} - -export interface PaginationQueryParameters { - page_token?: string; - limit?: string; - websockets?: boolean; -} - -export interface ChannelListQueryParameters extends PaginationQueryParameters { - include_users?: string; - search_query?: string; - mine?: boolean; -} - -export interface ChannelSearchQueryParameters extends PaginationQueryParameters { - q: string; -} - -export class CreateChannelBody { - options?: ChannelCreateOptions; - resource: ChannelCreateResource; -} - -export class ChannelCreateOptions { - /** - * Members to add to channel in case of direct channel - */ - members?: string[]; -} - -export type ChannelSaveOptions = ChannelCreateOptions & { - /** - * Add the channel creator as member. - */ - addCreatorAsMember?: boolean; -}; - -export class ChannelListOptions { - channels?: string[]; - mine?: boolean; - company_role?: string; - user_id?: string; -} - -export class UpdateChannelBody { - resource: ChannelUpdateResource; -} - -export class ReadChannelBody { - value: boolean; -} - -export type ChannelCreateResource = Pick; - -export type ChannelUpdateResource = Pick; - -// channel members - -export class CreateChannelMemberBody { - resource: Pick; -} - -export class UpdateChannelMemberBody { - resource: Pick; -} - -export type ChannelMemberSaveOptions = null; - -// channel tabs - -export interface ChannelTabParameters extends ChannelParameters { - tab_id: string; -} - -export class CreateChannelTabBody { - resource: ChannelTab; -} - -export class UpdateChannelTabBody { - resource: ChannelTab; -} - -export type ChannelTabSaveOptions = { - resource: ChannelTab; -}; - -export interface ChannelPendingEmailsParameters extends BaseChannelsParameters { - channel_id: string; -} - -export interface ChannelPendingEmailsDeleteQueryParameters extends ChannelPendingEmailsParameters { - email: ChannelPendingEmails["email"]; -} - -export interface ChannelPendingEmailsListQueryParameters extends BaseChannelsParameters { - channel_id: string; -} diff --git a/tdrive/backend/node/src/services/global-resolver.ts b/tdrive/backend/node/src/services/global-resolver.ts index bc8b519f..16d2f206 100644 --- a/tdrive/backend/node/src/services/global-resolver.ts +++ b/tdrive/backend/node/src/services/global-resolver.ts @@ -14,7 +14,6 @@ import { PushServiceAPI } from "../core/platform/services/push/api"; import { CronAPI } from "../core/platform/services/cron/api"; import WebSocketAPI from "../core/platform/services/websocket/provider"; import TrackerAPI from "../core/platform/services/tracker/provider"; -import KnowledgeGraphService from "../core/platform/services/knowledge-graph"; import EmailPusherAPI from "../core/platform/services/email-pusher/provider"; import { logger } from "../core/platform/framework"; @@ -22,26 +21,14 @@ import assert from "assert"; import { CompanyServiceImpl } from "./user/services/companies"; import { WorkspaceServiceImpl } from "./workspaces/services/workspace"; import { UserExternalLinksServiceImpl } from "./user/services/external_links"; -import { UserNotificationBadgeService } from "./notifications/services/bages"; -import { NotificationPreferencesService } from "./notifications/services/preferences"; import { UserServiceImpl } from "./user/services/users/service"; import { CompanyApplicationServiceImpl } from "./applications/services/company-applications"; import { ApplicationServiceImpl } from "./applications/services/applications"; import { FileServiceImpl } from "./files/services"; -import { ChannelServiceImpl } from "./channels/services/channel/service"; -import { MemberServiceImpl } from "./channels/services/member/service"; -import ChannelPendingEmailServiceImpl from "./channels/services/channel/pending-emails/service"; -import { TabServiceImpl } from "./channels/services/tab"; import { ConsoleServiceImpl } from "./console/service"; import { StatisticsServiceImpl } from "./statistics/service"; -import { NotificationEngine } from "./notifications/services/engine"; -import { MobilePushService } from "./notifications/services/mobile-push"; -import { ChannelMemberPreferencesServiceImpl } from "./notifications/services/channel-preferences"; -import { ChannelThreadUsersServiceImpl } from "./notifications/services/channel-thread-users"; import { ApplicationHooksService } from "./applications/services/hooks"; import OnlineServiceImpl from "./online/service"; -import { ChannelsMessageQueueListener } from "./channels/services/pubsub"; -import { UserNotificationDigestService } from "./notifications/services/digest"; import { DocumentsService } from "./documents/services"; import { DocumentsEngine } from "./documents/services/engine"; import { TagsService } from "./tags/services/tags"; @@ -58,7 +45,6 @@ type PlatformServices = { tracker: TrackerAPI; webserver: WebServerAPI; websocket: WebSocketAPI; - knowledgeGraph: KnowledgeGraphService; emailPusher: EmailPusherAPI; }; @@ -69,28 +55,12 @@ type TdriveServices = { console: ConsoleServiceImpl; statistics: StatisticsServiceImpl; externalUser: UserExternalLinksServiceImpl; - notifications: { - badges: UserNotificationBadgeService; - channelPreferences: ChannelMemberPreferencesServiceImpl; - channelThreads: ChannelThreadUsersServiceImpl; - engine: NotificationEngine; - preferences: NotificationPreferencesService; - mobilePush: MobilePushService; - digest: UserNotificationDigestService; - }; applications: { marketplaceApps: ApplicationServiceImpl; companyApps: CompanyApplicationServiceImpl; hooks: ApplicationHooksService; }; files: FileServiceImpl; - channels: { - channels: ChannelServiceImpl; - members: MemberServiceImpl; - pubsub: ChannelsMessageQueueListener; - }; - channelPendingEmail: ChannelPendingEmailServiceImpl; - tab: TabServiceImpl; online: OnlineServiceImpl; documents: { documents: DocumentsService; @@ -126,7 +96,6 @@ class GlobalResolver { tracker: platform.getProvider("tracker"), webserver: platform.getProvider("webserver"), websocket: platform.getProvider("websocket"), - knowledgeGraph: await new KnowledgeGraphService().init(), emailPusher: platform.getProvider("email-pusher"), }; @@ -144,28 +113,12 @@ class GlobalResolver { console: await new ConsoleServiceImpl().init(), statistics: await new StatisticsServiceImpl().init(), externalUser: await new UserExternalLinksServiceImpl().init(), - notifications: { - badges: await new UserNotificationBadgeService().init(platform), - channelPreferences: await new ChannelMemberPreferencesServiceImpl().init(), - channelThreads: await new ChannelThreadUsersServiceImpl().init(), - engine: await new NotificationEngine().init(), - preferences: await new NotificationPreferencesService().init(), - mobilePush: await new MobilePushService().init(), - digest: await new UserNotificationDigestService().init(), - }, applications: { marketplaceApps: await new ApplicationServiceImpl().init(), companyApps: await new CompanyApplicationServiceImpl().init(), hooks: await new ApplicationHooksService().init(), }, files: await new FileServiceImpl().init(), - channels: { - channels: await new ChannelServiceImpl().init(), - members: await new MemberServiceImpl().init(), - pubsub: await new ChannelsMessageQueueListener().init(), - }, - channelPendingEmail: await new ChannelPendingEmailServiceImpl().init(), - tab: await new TabServiceImpl().init(), online: await new OnlineServiceImpl().init(), documents: { documents: await new DocumentsService().init(), diff --git a/tdrive/backend/node/src/services/knowledge-graph-web/index.ts b/tdrive/backend/node/src/services/knowledge-graph-web/index.ts deleted file mode 100644 index 24e77a28..00000000 --- a/tdrive/backend/node/src/services/knowledge-graph-web/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Consumes, Prefix, TdriveService } from "../../core/platform/framework"; -import WebServerAPI from "../../core/platform/services/webserver/provider"; -import web from "./web/index"; - -@Prefix("/internal/services/knowledge-graph/v1") -@Consumes(["webserver"]) -export default class MessageService extends TdriveService { - version = "1"; - name = "knowledge-graph"; - - api(): undefined { - return undefined; - } - - public async doInit(): Promise { - const fastify = this.context.getProvider("webserver").getServer(); - - fastify.register((instance, _opts, next) => { - web(instance, { prefix: this.prefix }); - next(); - }); - - return this; - } -} diff --git a/tdrive/backend/node/src/services/knowledge-graph-web/web/index.ts b/tdrive/backend/node/src/services/knowledge-graph-web/web/index.ts deleted file mode 100644 index 60f69e05..00000000 --- a/tdrive/backend/node/src/services/knowledge-graph-web/web/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { FastifyInstance, FastifyRegisterOptions } from "fastify"; -import routes from "./routes"; - -export default ( - fastify: FastifyInstance, - options: FastifyRegisterOptions<{ - prefix: string; - }>, -): void => { - fastify.log.debug("Configuring /internal/services/knowledge-graph/v1 routes"); - fastify.register(routes, options); -}; diff --git a/tdrive/backend/node/src/services/knowledge-graph-web/web/routes.ts b/tdrive/backend/node/src/services/knowledge-graph-web/web/routes.ts deleted file mode 100644 index 53b27bdd..00000000 --- a/tdrive/backend/node/src/services/knowledge-graph-web/web/routes.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { FastifyInstance, FastifyPluginCallback, FastifyReply, FastifyRequest } from "fastify"; -import { KnowledgeGraphCallbackEvent } from "../../../core/platform/services/knowledge-graph/types"; -import gr from "../../global-resolver"; - -const routes: FastifyPluginCallback = (fastify: FastifyInstance, _, next) => { - fastify.route({ - method: "POST", - url: "/push", - handler: async ( - request: FastifyRequest<{ - Body: { - events: KnowledgeGraphCallbackEvent[]; - }; - }>, - reply: FastifyReply, - ): Promise => { - try { - for (let i = 0; i < request.body.events.length; i++) { - await gr.platformServices.knowledgeGraph.onCallbackEvent( - request.headers.authorization.split(" ")[1], - request.body.events[i], - ); - } - } catch (e) { - reply.status(500).send({ - status: "error", - error: "Internal server error", - message: e.message || "Unknown error", - }); - } - - reply.status(200).send({ - status: "success", - }); - }, - }); - - next(); -}; - -export default routes; diff --git a/tdrive/backend/node/src/services/notifications/entities/channel-member-notification-preferences.ts b/tdrive/backend/node/src/services/notifications/entities/channel-member-notification-preferences.ts deleted file mode 100644 index 9b20051a..00000000 --- a/tdrive/backend/node/src/services/notifications/entities/channel-member-notification-preferences.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Type } from "class-transformer"; -import { merge } from "lodash"; -import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; -import { ChannelMemberNotificationLevel } from "../../../services/channels/types"; - -export const TYPE = "channel_members_notification_preferences"; -@Entity(TYPE, { - primaryKey: [["company_id", "channel_id"], "user_id"], - type: TYPE, -}) -export class ChannelMemberNotificationPreference { - /** - * Primary key / Partition key - */ - @Type(() => String) - @Column("user_id", "uuid") - user_id: string; - - /** - * Primary key - */ - @Type(() => String) - @Column("company_id", "uuid") - company_id: string; - - /** - * Primary key - */ - @Type(() => String) - @Column("channel_id", "uuid") - channel_id: string; - - @Type(() => String) - @Column("preferences", "string") - preferences: ChannelMemberNotificationLevel = ChannelMemberNotificationLevel.ALL; - - @Column("last_read", "number") - last_read = 0; -} - -export type ChannelMemberNotificationPreferencePrimaryKey = Pick< - ChannelMemberNotificationPreference, - "company_id" | "channel_id" | "user_id" ->; - -export function getInstance( - preference: ChannelMemberNotificationPreference, -): ChannelMemberNotificationPreference { - return merge(new ChannelMemberNotificationPreference(), preference); -} diff --git a/tdrive/backend/node/src/services/notifications/entities/channel-thread-users.ts b/tdrive/backend/node/src/services/notifications/entities/channel-thread-users.ts deleted file mode 100644 index 95db4501..00000000 --- a/tdrive/backend/node/src/services/notifications/entities/channel-thread-users.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Type } from "class-transformer"; -import { merge } from "lodash"; -import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; - -export const TYPE = "channel_thread_users"; -@Entity(TYPE, { - primaryKey: [["company_id", "channel_id", "thread_id"], "user_id"], - type: TYPE, -}) -export class ChannelThreadUsers { - /** - * UUIDv4 - * Primary key / partition key - */ - @Type(() => String) - @Column("company_id", "uuid") - company_id: string; - - /** - * UUIDv4 - * Primary key / partition key - */ - @Type(() => String) - @Column("channel_id", "uuid") - channel_id: string; - - /** - * UUIDv4 - * Primary key / partition key - */ - @Type(() => String) - @Column("thread_id", "uuid") - thread_id: string; - - /** - * UUIDv4 - */ - @Type(() => String) - @Column("user_id", "uuid") - user_id: string; -} - -export type ChannelThreadUsersPrimaryKey = Pick< - ChannelThreadUsers, - "company_id" | "channel_id" | "thread_id" ->; - -export function getInstance(element: ChannelThreadUsers): ChannelThreadUsers { - return merge(new ChannelThreadUsers(), element); -} diff --git a/tdrive/backend/node/src/services/notifications/entities/index.ts b/tdrive/backend/node/src/services/notifications/entities/index.ts deleted file mode 100644 index bb1e7ead..00000000 --- a/tdrive/backend/node/src/services/notifications/entities/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -export { - ChannelMemberNotificationPreference, - ChannelMemberNotificationPreferencePrimaryKey, - TYPE as ChannelMemberNotificationPreferenceType, - getInstance as getNotificationPreferenceInstance, -} from "./channel-member-notification-preferences"; - -export { - ChannelThreadUsers, - ChannelThreadUsersPrimaryKey, - TYPE as ChannelThreadUsersType, - getInstance as getChannelThreadUsersInstance, -} from "./channel-thread-users"; - -export { - UserNotificationBadge, - UserNotificationBadgePrimaryKey, - TYPE as UserNotificationBadgeType, - getInstance as getUserNotificationBadgeInstance, -} from "./user-notification-badges"; diff --git a/tdrive/backend/node/src/services/notifications/entities/user-notification-badges.ts b/tdrive/backend/node/src/services/notifications/entities/user-notification-badges.ts deleted file mode 100644 index fb19ee47..00000000 --- a/tdrive/backend/node/src/services/notifications/entities/user-notification-badges.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Type } from "class-transformer"; -import { ChannelType } from "../../../utils/types"; -import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; -import { v4 as uuidv4 } from "uuid"; -import { merge } from "lodash"; - -export const TYPE = "user_notification_badges"; -/** - * Table user-notification-badges - */ -@Entity(TYPE, { - primaryKey: [["company_id"], "user_id", "workspace_id", "channel_id", "thread_id"], - type: TYPE, -}) -export class UserNotificationBadge { - /** - * UUIDv4 - * Primary key / partition key - */ - @Type(() => String) - @Column("company_id", "uuid") - company_id: string; - - /** - * UUIDv4 - */ - @Type(() => String) - @Column("user_id", "uuid") - user_id: string; - - /** - * Text - * Primary key - */ - @Type(() => String) - @Column("workspace_id", "string") - workspace_id: string | ChannelType.DIRECT; - - /** - * UUIDv4 - * Primary key - */ - @Type(() => String) - @Column("channel_id", "uuid") - channel_id: string; - - /** - * UUIDv4 - * Primary key - */ - @Type(() => String) - @Column("thread_id", "string") //It can be null - thread_id: string; - - @Type(() => String) - @Column("message_id", "string") //It can be null - message_id: string; - - /** - * UUIDv4 - * Only used because crud entities must always have an id - */ - @Type(() => String) - @Column("id", "uuid") - id: string = uuidv4(); - - @Column("mention_type", "string") - mention_type: "reply" | "global" | "me" | "unread" | null = "me"; -} - -export type UserNotificationBadgePrimaryKey = Pick< - UserNotificationBadge, - "user_id" | "company_id" | "workspace_id" | "channel_id" | "thread_id" ->; - -export function getInstance(badge: UserNotificationBadge): UserNotificationBadge { - return merge(new UserNotificationBadge(), badge); -} diff --git a/tdrive/backend/node/src/services/notifications/entities/user-notification-digest.ts b/tdrive/backend/node/src/services/notifications/entities/user-notification-digest.ts deleted file mode 100644 index cbbcfdde..00000000 --- a/tdrive/backend/node/src/services/notifications/entities/user-notification-digest.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Type } from "class-transformer"; -import { merge } from "lodash"; -import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; - -export const TYPE = "user_notification_digest"; -/** - * Table user-notification-digest - */ -@Entity(TYPE, { - primaryKey: [["company_id"], "user_id"], - type: TYPE, -}) -export class UserNotificationDigest { - /** - * UUIDv4 - * Primary key / partition key - */ - @Type(() => String) - @Column("company_id", "uuid") - company_id: string; - - /** - * UUIDv4 - */ - @Type(() => String) - @Column("user_id", "uuid") - user_id: string; - - @Column("created_at", "number") - created_at: number; - - @Column("deliver_at", "number") - deliver_at: number; -} - -export type UserNotificationDigestPrimaryKey = Pick< - UserNotificationDigest, - "user_id" | "company_id" ->; - -export function getInstance(digest: UserNotificationDigest): UserNotificationDigest { - return merge(new UserNotificationDigest(), digest); -} diff --git a/tdrive/backend/node/src/services/notifications/index.ts b/tdrive/backend/node/src/services/notifications/index.ts deleted file mode 100644 index eff45ed0..00000000 --- a/tdrive/backend/node/src/services/notifications/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Prefix, TdriveService } from "../../core/platform/framework"; -import WebServerAPI from "../../core/platform/services/webserver/provider"; -import web from "./web"; - -@Prefix("/internal/services/notifications/v1") -export default class NotificationService extends TdriveService { - version = "1"; - name = "notifications"; - - public async doInit(): Promise { - const fastify = this.context.getProvider("webserver").getServer(); - fastify.register((instance, _opts, next) => { - web(instance, { prefix: this.prefix }); - next(); - }); - return this; - } - // TODO: remove - api(): undefined { - return undefined; - } -} diff --git a/tdrive/backend/node/src/services/notifications/notifiers/index.ts b/tdrive/backend/node/src/services/notifications/notifiers/index.ts deleted file mode 100644 index 23899d66..00000000 --- a/tdrive/backend/node/src/services/notifications/notifiers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./mobile-push"; - -export interface Notifier { - notify(user: string, message: Message): Promise; -} diff --git a/tdrive/backend/node/src/services/notifications/notifiers/mobile-push.ts b/tdrive/backend/node/src/services/notifications/notifiers/mobile-push.ts deleted file mode 100644 index 2d8e7ca4..00000000 --- a/tdrive/backend/node/src/services/notifications/notifiers/mobile-push.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { logger } from "../../../core/platform/framework"; -import { MessageQueueServiceAPI } from "../../../core/platform/services/message-queue/api"; -import { Notifier } from "."; - -const TOPIC = "notification:push:mobile"; - -export class MobilePushNotifier implements Notifier { - private static instance: MobilePushNotifier; - - static get(messageQueue: MessageQueueServiceAPI): Notifier { - if (!MobilePushNotifier.instance) { - MobilePushNotifier.instance = new MobilePushNotifier(messageQueue); - } - - return MobilePushNotifier.instance; - } - - private constructor(private messageQueue: MessageQueueServiceAPI) {} - - async notify(user: string, message: Message): Promise { - logger.info(`MobilePushNotifier - Push to mobile ${user}`); - logger.debug(`MobilePushNotifier - Push to mobile ${user}, ${JSON.stringify(message)}`); - - try { - await this.messageQueue.publish(TOPIC, { - data: message, - }); - } catch (err) { - logger.warn({ err }, `MobilePushNotifier - Error while sending notification to user ${user}`); - } - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/bages.ts b/tdrive/backend/node/src/services/notifications/services/bages.ts deleted file mode 100644 index 572c7264..00000000 --- a/tdrive/backend/node/src/services/notifications/services/bages.ts +++ /dev/null @@ -1,261 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { - Initializable, - RealtimeDeleted, - RealtimeSaved, - TdriveContext, - TdriveServiceProvider, -} from "../../../core/platform/framework"; -import { ResourcePath } from "../../../core/platform/services/realtime/types"; -import { - CrudException, - DeleteResult, - ExecutionContext, - ListResult, - OperationType, - Pagination, - SaveResult, -} from "../../../core/platform/framework/api/crud-service"; -import { - getUserNotificationBadgeInstance, - UserNotificationBadge, - UserNotificationBadgePrimaryKey, - UserNotificationBadgeType, -} from "../entities"; -import { NotificationExecutionContext } from "../types"; -import { getNotificationRoomName } from "./realtime"; -import Repository from "../../../core/platform/services/database/services/orm/repository/repository"; -import gr from "../../global-resolver"; -import _, { pick, uniq } from "lodash"; - -export class UserNotificationBadgeService implements TdriveServiceProvider, Initializable { - version: "1"; - repository: Repository; - - async init(context: TdriveContext): Promise { - this.repository = await gr.database.getRepository( - UserNotificationBadgeType, - UserNotificationBadge, - ); - - return this; - } - - async get( - pk: UserNotificationBadgePrimaryKey, - context: ExecutionContext, - ): Promise { - return await this.repository.findOne(pk, {}, context); - } - - @RealtimeSaved((badge, context) => { - return [ - { - room: ResourcePath.get(getNotificationRoomName(badge.user_id)), - }, - ]; - }) - async save( - badge: UserNotificationBadge, - context: ExecutionContext, - ): Promise> { - //Initiate the digest - await gr.services.notifications.digest.putBadge(badge); - - await this.repository.save(getUserNotificationBadgeInstance(badge), context); - return new SaveResult(UserNotificationBadgeType, badge, OperationType.CREATE); - } - - @RealtimeDeleted((badge, context) => { - return [ - { - room: ResourcePath.get(getNotificationRoomName(badge.user_id)), - }, - ]; - }) - async delete( - pk: UserNotificationBadgePrimaryKey, - context?: NotificationExecutionContext, - ): Promise> { - //Cancel the current digest as we just read the badges - await gr.services.notifications.digest.cancelDigest(pk.company_id, pk.user_id); - - await this.repository.remove(pk as UserNotificationBadge, context); - return new DeleteResult(UserNotificationBadgeType, pk as UserNotificationBadge, true); - } - - list(): Promise> { - throw new Error("Not implemented"); - } - - async listForUserPerCompanies( - user_id: string, - context: ExecutionContext, - ): Promise> { - //We remove all badge from current company as next block will create dupicates - const companies_ids = (await gr.services.companies.getAllForUser(user_id)).map( - gu => gu.group_id, - ); - - let result: UserNotificationBadge[] = []; - let type = ""; - for (const company_id of companies_ids) { - const find = await this.repository.find( - { - company_id, - user_id, - }, - { - pagination: new Pagination("", "1"), - }, - context, - ); - type = find.type; - result = result.concat(find.getEntities()); - } - - const badges = new ListResult(type, result); - await this.ensureBadgesAreReachable(badges, context); - - return badges; - } - - async listForUser( - company_id: string, - user_id: string, - filter: Pick, - context?: ExecutionContext, - ): Promise> { - if (!company_id || !user_id) { - throw CrudException.badRequest("company_id and user_id are required"); - } - - //Cancel the current digest as we just read the badges - await gr.services.notifications.digest.cancelDigest(company_id, user_id); - - const badges = await this.repository.find( - { - ...{ - company_id, - user_id, - }, - ...pick(filter, ["workspace_id", "channel_id", "thread_id"]), - }, - {}, - context, - ); - - await this.ensureBadgesAreReachable(badges, context); - - return badges; - } - - // This will ensure we are still in the channels and if not, we'll remove the badge - // We need to also ensure more than that - // - Are we in the workspace? - // - Are we in the company? - async ensureBadgesAreReachable( - badges: ListResult, - context?: ExecutionContext, - ): Promise> { - if (badges.getEntities().length === 0) { - return badges; - } - - const userId = badges.getEntities()[0].user_id; - - const channels = uniq(badges.getEntities().map(r => r.channel_id)); - for (const channelId of channels) { - const someBadge = badges.getEntities().find(b => b.channel_id === channelId); - const channelMemberPk = { - company_id: someBadge.company_id, - workspace_id: someBadge.workspace_id, - channel_id: channelId, - user_id: userId, - }; - const context = { - user: { id: channelMemberPk.user_id, server_request: true }, - channel: { id: channelId, ...channelMemberPk }, - }; - const exists = - (await gr.services.channels.channels.get( - { - id: channelId, - ..._.pick(channelMemberPk, "company_id", "workspace_id"), - }, - context, - )) && (await gr.services.channels.members.get(channelMemberPk, context)); - if (!exists) { - for (const badge of badges.getEntities()) { - if (badge.channel_id === channelId) this.removeUserChannelBadges(badge, context); - } - badges.filterEntities(b => b.channel_id !== channelId); - } - } - - const badgePerWorkspace = _.uniqBy(badges.getEntities(), r => r.workspace_id); - for (const badge of badgePerWorkspace) { - const workspaceId = badge.workspace_id; - const companyId = badge.company_id; - if (!workspaceId || workspaceId === "direct") { - continue; - } - try { - const exists = - (await gr.services.workspaces.get({ - id: workspaceId, - company_id: companyId, - })) && - (await gr.services.workspaces.getUser({ - workspaceId, - userId, - })); - if (!exists) { - await gr.services.channels.members.ensureUserNotInWorkspaceIsNotInChannel( - { id: userId }, - { id: workspaceId, company_id: companyId }, - context, - ); - for (const badge of badges.getEntities()) { - if (badge.workspace_id === workspaceId) this.removeUserChannelBadges(badge, context); - } - badges.filterEntities(b => b.workspace_id !== workspaceId); - } - } catch (e) {} - } - - return badges; - } - - /** - * FIXME: This is a temporary implementation which is sending as many websocket notifications as there are badges to remove - * A better implementation will be to do a bulk delete and have a single websocket notification event - * @param filter - * @param context - */ - async removeUserChannelBadges( - filter: Pick< - UserNotificationBadgePrimaryKey, - "workspace_id" | "company_id" | "channel_id" | "user_id" - >, - context?: ExecutionContext, - ): Promise { - const badges = ( - await this.repository.find( - _.pick(filter, ["workspace_id", "company_id", "channel_id", "user_id"]), - {}, - context, - ) - ).getEntities(); - - return ( - await Promise.all( - badges.map(async badge => { - try { - return (await this.delete(badge)).deleted; - } catch (err) {} - }), - ) - ).filter(Boolean).length; - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/channel-preferences.ts b/tdrive/backend/node/src/services/notifications/services/channel-preferences.ts deleted file mode 100644 index e876b3ba..00000000 --- a/tdrive/backend/node/src/services/notifications/services/channel-preferences.ts +++ /dev/null @@ -1,157 +0,0 @@ -import _ from "lodash"; -import { - DeleteResult, - ExecutionContext, - ListResult, - OperationType, - SaveResult, -} from "../../../core/platform/framework/api/crud-service"; -import { - ChannelMemberNotificationPreference, - ChannelMemberNotificationPreferencePrimaryKey, -} from "../entities"; -import Repository from "../../../core/platform/services/database/services/orm/repository/repository"; -import { Initializable, logger, TdriveServiceProvider } from "../../../core/platform/framework"; -import gr from "../../global-resolver"; - -const TYPE = "channel_members_notification_preferences"; - -export class ChannelMemberPreferencesServiceImpl implements TdriveServiceProvider, Initializable { - version: "1"; - repository: Repository; - - async init(): Promise { - this.repository = await gr.database.getRepository( - TYPE, - ChannelMemberNotificationPreference, - ); - - return this; - } - - async save( - entity: ChannelMemberNotificationPreference, - context: ExecutionContext, - ): Promise> { - const pk: ChannelMemberNotificationPreferencePrimaryKey = { - user_id: entity.user_id, - company_id: entity.company_id, - channel_id: entity.channel_id, - }; - - let preference = await this.repository.findOne(pk, {}, context); - - if (!preference) { - preference = new ChannelMemberNotificationPreference(); - preference = _.merge(entity, pk); - } - - preference = _.merge(preference, entity); - - await this.repository.save(preference, context); - - return new SaveResult(TYPE, preference, OperationType.CREATE); - } - - async get( - pk: ChannelMemberNotificationPreferencePrimaryKey, - context: ExecutionContext, - ): Promise { - return await this.repository.findOne(pk, {}, context); - } - - async delete( - pk: ChannelMemberNotificationPreferencePrimaryKey, - context?: ExecutionContext, - ): Promise> { - await this.repository.remove(pk as ChannelMemberNotificationPreference, context); - - return new DeleteResult(TYPE, pk as ChannelMemberNotificationPreference, true); - } - - list(): Promise> { - throw new Error("Not implemented"); - } - - async getChannelPreferencesForUsers( - channelAndCompany: Pick< - ChannelMemberNotificationPreferencePrimaryKey, - "channel_id" | "company_id" - >, - users?: string[] | null, - lastRead?: { - lessThan: number; - }, - context?: ExecutionContext, - ): Promise> { - logger.debug( - `ChannelMemberPreferenceService - Get Channel preferences for users ${JSON.stringify( - users, - )} with lastRead < ${lastRead?.lessThan}`, - ); - const result = await this.repository.find( - users ? { ...channelAndCompany, ...{ user_id: users } } : channelAndCompany, - {}, - context, - ); - - if (result.getEntities().length > 0 && lastRead && lastRead.lessThan) { - result.filterEntities(entity => entity.last_read < lastRead.lessThan); - } - - logger.debug( - `ChannelMemberPreferenceService - Result ${JSON.stringify( - result.getEntities().map(preference => preference.user_id), - )}`, - ); - - return result; - } - - async getChannelPreferencesForUsersFilteredByReadTime( - channelAndCompany: Pick< - ChannelMemberNotificationPreferencePrimaryKey, - "channel_id" | "company_id" - >, - users: string[] = [], - lastRead: number, - context?: ExecutionContext, - ): Promise> { - const result = await this.repository.find( - { ...channelAndCompany, ...{ user_id: users } }, - {}, - context, - ); - - if (result.getEntities().length > 0 && lastRead) { - result.filterEntities(entity => entity.last_read < lastRead); - } - - return result; - } - - async updateLastRead( - channelAndCompany: Pick, - user_id: string, - lastRead: number, - context?: ExecutionContext, - ): Promise { - const pk: ChannelMemberNotificationPreferencePrimaryKey = { - user_id, - company_id: channelAndCompany.company_id, - channel_id: channelAndCompany.channel_id, - }; - - const preference = await this.repository.findOne(pk, {}, context); - - if (!preference) { - return; - } - - preference.last_read = lastRead; - - await this.repository.save(preference, context); - - return preference; - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/channel-thread-users.ts b/tdrive/backend/node/src/services/notifications/services/channel-thread-users.ts deleted file mode 100644 index a96918ec..00000000 --- a/tdrive/backend/node/src/services/notifications/services/channel-thread-users.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - DeleteResult, - ExecutionContext, - ListResult, - OperationType, - SaveResult, -} from "../../../core/platform/framework/api/crud-service"; -import { - ChannelThreadUsers, - ChannelThreadUsersPrimaryKey, - ChannelThreadUsersType, -} from "../entities"; -import Repository from "../../../core/platform/services/database/services/orm/repository/repository"; -import gr from "../../global-resolver"; -import { Initializable, TdriveServiceProvider } from "../../../core/platform/framework"; - -export class ChannelThreadUsersServiceImpl implements TdriveServiceProvider, Initializable { - version: "1"; - repository: Repository; - - async init(): Promise { - this.repository = await gr.database.getRepository( - ChannelThreadUsersType, - ChannelThreadUsers, - ); - - return this; - } - - async bulkSave( - entities: ChannelThreadUsers[], - context: ExecutionContext, - ): Promise> { - await this.repository.saveAll(entities, context); - - return new SaveResult(ChannelThreadUsersType, entities, OperationType.CREATE); - } - - async get( - pk: ChannelThreadUsersPrimaryKey, - context: ExecutionContext, - ): Promise { - return await this.repository.findOne(pk, {}, context); - } - - async delete( - pk: ChannelThreadUsersPrimaryKey, - context: ExecutionContext, - ): Promise> { - await this.repository.remove(pk as ChannelThreadUsers, context); - - return new DeleteResult(ChannelThreadUsersType, pk as ChannelThreadUsers, true); - } - - list(): Promise> { - throw new Error("Not implemented"); - } - - async getUsersInThread( - pk: ChannelThreadUsersPrimaryKey, - context: ExecutionContext, - ): Promise> { - if (!pk.company_id || !pk.channel_id || !pk.thread_id) { - throw new Error("Missing required fields"); - } - return this.repository.find(pk, {}, context); - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/digest.ts b/tdrive/backend/node/src/services/notifications/services/digest.ts deleted file mode 100644 index b6e01bf7..00000000 --- a/tdrive/backend/node/src/services/notifications/services/digest.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Initializable, TdriveServiceProvider } from "../../../core/platform/framework"; -import { Paginable, Pagination } from "../../../core/platform/framework/api/crud-service"; -import Repository from "../../../core/platform/services/database/services/orm/repository/repository"; -import { Channel } from "../../../services/channels/entities"; -import Workspace from "../../../services/workspaces/entities/workspace"; -import gr from "../../global-resolver"; -import { UserNotificationBadge } from "../entities"; -import { getInstance, TYPE, UserNotificationDigest } from "../entities/user-notification-digest"; - -export class UserNotificationDigestService implements TdriveServiceProvider, Initializable { - version: "1"; - repository: Repository; - - async init(): Promise { - this.repository = await gr.database.getRepository( - TYPE, - UserNotificationDigest, - ); - - gr.platformServices.cron.schedule( - "*/15 * * * *", - async () => { - //This being multi-node we will try to avoid running them at the exact same time - //Fixme: this is until we find a better solution of course - await new Promise(r => setTimeout(r, 1000 * 60 * Math.random())); - await this.processDigests(); - }, - "Find and process digests", - ); - - return this; - } - - async getDigest(companyId: string, userId: string): Promise { - return await this.repository.findOne({ company_id: companyId, user_id: userId }); - } - - async cancelDigest(companyId: string, userId: string) { - const digest = await this.getDigest(companyId, userId); - if (digest) await this.repository.remove(digest); - } - - async putBadge(badge: UserNotificationBadge): Promise { - const digest = await this.getDigest(badge.company_id, badge.user_id); - let deliver_at = digest?.deliver_at; - if (!deliver_at) { - //Get user preferences to set the deliver at - const preferences = await gr.services.notifications.preferences.getMerged({ - company_id: badge.company_id, - user_id: badge.user_id, - workspace_id: "all", - }); - let emailNotificationsDelay = preferences?.preferences?.email_notifications_delay; - if (emailNotificationsDelay === undefined) emailNotificationsDelay = 15; - if (!emailNotificationsDelay) { - return; - } else { - deliver_at = Date.now() + emailNotificationsDelay * 1000 * 60; - } - } - await this.repository.save( - getInstance({ - company_id: badge.company_id, - user_id: badge.user_id, - created_at: digest?.created_at || Date.now(), - deliver_at, - }), - ); - } - - async processDigests(): Promise { - let digestsPagination: Paginable = new Pagination(null, "100"); - do { - const digests = await this.repository.find({}); - const digestsEntities = digests.getEntities().filter(d => { - return d.deliver_at < Date.now(); - }); - - for (const digest of digestsEntities) { - await this.repository.remove(digest); - } - - for (const digest of digestsEntities) { - await this.processDigest(digest); - } - - digestsPagination = new Pagination(digests.nextPage.page_token, "100"); - } while (digestsPagination.page_token); - } - - async processDigest(digest: UserNotificationDigest): Promise { - const badges = await gr.services.notifications.badges.listForUser( - digest.company_id, - digest.user_id, - {} as any, - ); - - const user = await gr.services.users.get({ id: digest.user_id }); - const company = await gr.services.companies.getCompany({ id: digest.company_id }); - const notifications: { - channel: Channel; - workspace: Workspace; - }[] = []; - const workspaces: { [key: string]: Workspace } = {}; - const channels: { [key: string]: Channel } = {}; - - for (const badge of badges.getEntities()) { - if (!badge.thread_id) continue; - try { - channels[badge.channel_id] = - channels[badge.channel_id] || - (await gr.services.channels.channels.get({ - company_id: badge.company_id, - workspace_id: badge.workspace_id, - id: badge.channel_id, - })); - - workspaces[badge.workspace_id] = - badge.workspace_id && badge.workspace_id !== "direct" - ? workspaces[badge.workspace_id] || - (await gr.services.workspaces.get({ - company_id: badge.company_id, - id: badge.workspace_id, - })) - : null; - - notifications.push({ - channel: channels[badge.channel_id], - workspace: workspaces[badge.workspace_id], - }); - } catch (e) {} - } - - const etaEntry = { - user, - company, - notifications, - }; - - if (notifications.length > 0) { - const { html, text, subject } = await gr.platformServices.emailPusher.build( - "notification-digest", - user.language, - etaEntry, - ); - await gr.platformServices.emailPusher.send(user.email_canonical, { - subject, - html, - text, - }); - } - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/engine/index.ts b/tdrive/backend/node/src/services/notifications/services/engine/index.ts deleted file mode 100644 index 53a7ceb9..00000000 --- a/tdrive/backend/node/src/services/notifications/services/engine/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Initializable } from "../../../../core/platform/framework"; -import { MarkChannelAsReadMessageProcessor } from "./processors/mark-channel-as-read"; -import { MarkChannelAsUnreadMessageProcessor } from "./processors/mark-channel-as-unread"; -import { PushNotificationMessageProcessor } from "./processors/mobile-push-notifications"; -import { PushNotificationToUsersMessageProcessor } from "./processors/push-to-users"; -import { LeaveChannelMessageProcessor } from "./processors/channel-member-deleted"; -import { JoinChannelMessageProcessor } from "./processors/channel-member-created"; -import { UpdateChannelMemberMessageProcessor } from "./processors/channel-member-updated"; -import { PushReactionNotification } from "./processors/reaction-notification"; -import gr from "../../../global-resolver"; - -/** - * The notification engine is in charge of processing data and delivering user notifications on the right place - */ -export class NotificationEngine implements Initializable { - async init(): Promise { - gr.platformServices.messageQueue.processor.addHandler( - new UpdateChannelMemberMessageProcessor(), - ); - gr.platformServices.messageQueue.processor.addHandler(new JoinChannelMessageProcessor()); - gr.platformServices.messageQueue.processor.addHandler(new LeaveChannelMessageProcessor()); - gr.platformServices.messageQueue.processor.addHandler(new MarkChannelAsReadMessageProcessor()); - gr.platformServices.messageQueue.processor.addHandler( - new MarkChannelAsUnreadMessageProcessor(), - ); - gr.platformServices.messageQueue.processor.addHandler(new PushNotificationMessageProcessor()); - gr.platformServices.messageQueue.processor.addHandler( - new PushNotificationToUsersMessageProcessor(), - ); - gr.platformServices.messageQueue.processor.addHandler(new PushReactionNotification()); - - return this; - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/engine/processors/channel-member-created.ts b/tdrive/backend/node/src/services/notifications/services/engine/processors/channel-member-created.ts deleted file mode 100644 index a12e6466..00000000 --- a/tdrive/backend/node/src/services/notifications/services/engine/processors/channel-member-created.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - ChannelMemberNotificationPreference, - getNotificationPreferenceInstance, -} from "../../../entities"; -import { logger } from "../../../../../core/platform/framework"; -import { Channel, ChannelMember } from "../../../../channels/entities"; -import gr from "../../../../global-resolver"; -import { NotificationMessageQueueHandler } from "../../../types"; -import { ExecutionContext } from "../../../../../core/platform/framework/api/crud-service"; - -type JoinChannelMessage = { channel: Channel; member: ChannelMember }; - -export class JoinChannelMessageProcessor - implements NotificationMessageQueueHandler -{ - readonly topics = { - in: "channel:member:created", - }; - - readonly options = { - unique: true, - ack: true, - }; - - readonly name = "JoinChannelMessageProcessor"; - - validate(message: JoinChannelMessage): boolean { - return !!(message && message.channel && message.member); - } - - async process(message: JoinChannelMessage, context?: ExecutionContext): Promise { - logger.info( - `${this.name} - Processing join channel message for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - - try { - let preference: ChannelMemberNotificationPreference; - - if (Channel.isDirectChannel(message.channel)) { - // Check if the user already have preference for this channel in case he already joined it before - preference = await gr.services.notifications.channelPreferences.get( - { - company_id: message.member.company_id, - channel_id: message.member.channel_id, - user_id: message.member.user_id, - }, - context, - ); - } - - if (preference) { - logger.info( - `${this.name} - Notification preference already exists for user ${message.member.user_id} in direct channel ${message.channel.id}`, - ); - - return; - } - - preference = getNotificationPreferenceInstance({ - channel_id: message.member.channel_id, - company_id: message.member.company_id, - user_id: message.member.user_id, - last_read: message.member.last_access || 0, - preferences: message.member.notification_level, - }); - - await gr.services.notifications.channelPreferences.save(preference, context); - - logger.info( - `${this.name} - Notification preference has been created for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - } catch (err) { - logger.warn( - { err }, - `${this.name} - Error while creating notification preference from pubsub`, - ); - } - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/engine/processors/channel-member-deleted.ts b/tdrive/backend/node/src/services/notifications/services/engine/processors/channel-member-deleted.ts deleted file mode 100644 index fa7c873d..00000000 --- a/tdrive/backend/node/src/services/notifications/services/engine/processors/channel-member-deleted.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { assign, merge } from "lodash"; -import { ChannelMemberNotificationPreference, UserNotificationBadge } from "../../../entities"; -import { logger } from "../../../../../core/platform/framework"; -import { Channel, ChannelMember } from "../../../../channels/entities"; -import { isDirectChannel } from "../../../../channels/utils"; -import gr from "../../../../global-resolver"; -import { NotificationMessageQueueHandler } from "../../../types"; -import { ExecutionContext } from "../../../../../core/platform/framework/api/crud-service"; - -type LeaveChannelMessage = { channel: Channel; member: ChannelMember }; - -export class LeaveChannelMessageProcessor - implements NotificationMessageQueueHandler -{ - readonly topics = { - in: "channel:member:deleted", - }; - - readonly options = { - unique: true, - ack: true, - }; - - readonly name = "LeaveChannelMessageProcessor"; - - validate(message: LeaveChannelMessage): boolean { - return !!(message && message.channel && message.member); - } - - async process(message: LeaveChannelMessage, context?: ExecutionContext): Promise { - logger.info( - `${this.name} - Processing leave channel message for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - - if (Channel.isDirectChannel(message.channel)) { - logger.info( - `${this.name} - Channel is direct, do not clean resources for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - return; - } - - await Promise.all([this.removeBadges(message, context), this.removePreferences(message)]); - } - - async removeBadges(message: LeaveChannelMessage, context: ExecutionContext): Promise { - logger.info( - `${this.name} - Removing badges for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - - try { - const badgeEntity = new UserNotificationBadge(); - assign(badgeEntity, { - workspace_id: message.channel.workspace_id, - company_id: message.channel.company_id, - channel_id: message.channel.id, - user_id: message.member.user_id, - }); - const removedBadges = await gr.services.notifications.badges.removeUserChannelBadges( - badgeEntity, - context, - ); - - logger.info( - `${this.name} - Removed ${removedBadges} badges for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - } catch (err) { - logger.warn( - { err }, - `${this.name} - Error while removing badges for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - } - } - - async removePreferences(message: LeaveChannelMessage): Promise { - if (isDirectChannel(message.channel)) { - logger.info( - `${this.name} - Notification preference was kept for user ${message.member.user_id} in direct channel ${message.channel.id}`, - ); - return; - } - - try { - const preference = merge(new ChannelMemberNotificationPreference(), { - channel_id: message.member.channel_id, - company_id: message.member.company_id, - user_id: message.member.user_id, - }); - - await gr.services.notifications.channelPreferences.delete(preference); - - logger.info( - `${this.name} - Notification preference has been deleted for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - } catch (err) { - logger.warn( - { err }, - `${this.name} - Error while removing preferences for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - } - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/engine/processors/channel-member-updated.ts b/tdrive/backend/node/src/services/notifications/services/engine/processors/channel-member-updated.ts deleted file mode 100644 index 69904e15..00000000 --- a/tdrive/backend/node/src/services/notifications/services/engine/processors/channel-member-updated.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { merge } from "lodash"; -import { ChannelMemberNotificationPreference } from "../../../entities"; -import { logger } from "../../../../../core/platform/framework"; -import { Channel, ChannelMember } from "../../../../channels/entities"; -import gr from "../../../../global-resolver"; -import { NotificationMessageQueueHandler } from "../../../types"; -import { ExecutionContext } from "../../../../../core/platform/framework/api/crud-service"; - -type UpdateChannelMessage = { channel: Channel; member: ChannelMember }; - -export class UpdateChannelMemberMessageProcessor - implements NotificationMessageQueueHandler -{ - readonly topics = { - in: "channel:member:updated", - }; - - readonly options = { - unique: true, - ack: true, - }; - - readonly name = "UpdateChannelMemberMessageProcessor"; - - validate(message: UpdateChannelMessage): boolean { - return !!(message && message.channel && message.member); - } - - async process(message: UpdateChannelMessage, context?: ExecutionContext): Promise { - logger.info( - `${this.name} - Processing update channel member message for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - - const preference = merge(new ChannelMemberNotificationPreference(), { - channel_id: message.member.channel_id, - company_id: message.member.company_id, - user_id: message.member.user_id, - last_read: message.member.last_access || 0, - preferences: message.member.notification_level, - }); - - try { - await gr.services.notifications.channelPreferences.save(preference, context); - - logger.info( - `${this.name} - Channel member notification preference has been updated for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - } catch (err) { - logger.error( - { err }, - `${this.name} - Error while updating channel member notification preference for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - } - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/engine/processors/mark-channel-as-read.ts b/tdrive/backend/node/src/services/notifications/services/engine/processors/mark-channel-as-read.ts deleted file mode 100644 index 46d7e27f..00000000 --- a/tdrive/backend/node/src/services/notifications/services/engine/processors/mark-channel-as-read.ts +++ /dev/null @@ -1,70 +0,0 @@ -import _ from "lodash"; -import { UserNotificationBadge } from "../../../entities"; -import { logger } from "../../../../../core/platform/framework"; -import { ChannelReadMessage, NotificationMessageQueueHandler } from "../../../types"; -import gr from "../../../../global-resolver"; -import { ExecutionContext } from "../../../../../core/platform/framework/api/crud-service"; - -export class MarkChannelAsReadMessageProcessor - implements NotificationMessageQueueHandler -{ - readonly topics = { - in: "channel:read", - }; - - readonly options = { - unique: true, - ack: true, - }; - - readonly name = "MarkChannelAsReadMessageProcessor"; - - validate(message: ChannelReadMessage): boolean { - return !!( - message && - message.channel && - message.channel.workspace_id && - message.channel.company_id && - message.channel.id && - message.member && - message.member.user_id - ); - } - - async process(message: ChannelReadMessage, context?: ExecutionContext): Promise { - logger.info( - `${this.name} - Processing message for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - - await this.removeBadges(message, context); - } - - async removeBadges(message: ChannelReadMessage, context: ExecutionContext): Promise { - logger.info( - `${this.name} - Removing badges for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - - try { - const badgeEntity = new UserNotificationBadge(); - _.assign(badgeEntity, { - workspace_id: message.channel.workspace_id, - company_id: message.channel.company_id, - channel_id: message.channel.id, - user_id: message.member.user_id, - }); - const removedBadges = await gr.services.notifications.badges.removeUserChannelBadges( - badgeEntity, - context, - ); - - logger.info( - `${this.name} - Removed ${removedBadges} badges for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - } catch (err) { - logger.warn( - { err }, - `${this.name} - Error while removing badges for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - } - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/engine/processors/mark-channel-as-unread.ts b/tdrive/backend/node/src/services/notifications/services/engine/processors/mark-channel-as-unread.ts deleted file mode 100644 index bb83b1e9..00000000 --- a/tdrive/backend/node/src/services/notifications/services/engine/processors/mark-channel-as-unread.ts +++ /dev/null @@ -1,68 +0,0 @@ -import _ from "lodash"; -import { UserNotificationBadge } from "../../../entities"; -import { logger } from "../../../../../core/platform/framework"; -import { ChannelUnreadMessage, NotificationMessageQueueHandler } from "../../../types"; -import gr from "../../../../global-resolver"; -import { ExecutionContext } from "../../../../../core/platform/framework/api/crud-service"; - -export class MarkChannelAsUnreadMessageProcessor - implements NotificationMessageQueueHandler -{ - readonly topics = { - in: "channel:unread", - }; - - readonly options = { - unique: true, - ack: true, - }; - - readonly name = "MarkChannelAsUnreadMessageProcessor"; - - validate(message: ChannelUnreadMessage): boolean { - return !!( - message && - message.channel && - message.channel.workspace_id && - message.channel.company_id && - message.channel.id && - message.member && - message.member.user_id - ); - } - - async process(message: ChannelUnreadMessage, context?: ExecutionContext): Promise { - logger.info( - `${this.name} - Processing message for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - - await this.addBadge(message, context); - } - - async addBadge(message: ChannelUnreadMessage, context: ExecutionContext): Promise { - logger.info( - `${this.name} - Creating a badge for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - - try { - const badgeEntity = new UserNotificationBadge(); - _.assign(badgeEntity, { - workspace_id: message.channel.workspace_id, - company_id: message.channel.company_id, - channel_id: message.channel.id, - user_id: message.member.user_id, - mention_type: "unread", - }); - gr.services.notifications.badges.save(badgeEntity, context); - - logger.info( - `${this.name} - Created new badge for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - } catch (err) { - logger.warn( - { err }, - `${this.name} - Error while creating new badge for user ${message.member.user_id} in channel ${message.channel.id}`, - ); - } - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/engine/processors/mobile-push-notifications.ts b/tdrive/backend/node/src/services/notifications/services/engine/processors/mobile-push-notifications.ts deleted file mode 100644 index 5ba22d58..00000000 --- a/tdrive/backend/node/src/services/notifications/services/engine/processors/mobile-push-notifications.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { logger } from "../../../../../core/platform/framework"; -import { - NotificationMessageQueueHandler, - PushNotificationMessage, - PushNotificationMessageResult, -} from "../../../types"; -import gr from "../../../../global-resolver"; - -/** - * Push new message notification to a set of users - */ -export class PushNotificationMessageProcessor - implements - NotificationMessageQueueHandler -{ - readonly topics = { - in: "notification:push:mobile", - }; - - readonly options = { - unique: true, - ack: true, - }; - - name = "PushMobileNotificationProcessor"; - - validate(message: PushNotificationMessage): boolean { - return !!( - message && - message.channel_id && - message.company_id && - message.workspace_id && - message.message_id && - message.badge_value && - message.user - ); - } - - async process(message: PushNotificationMessage): Promise { - logger.info( - `${this.name} - Processing mobile push notification for channel ${message.channel_id}`, - ); - - await gr.services.notifications.mobilePush.push(message); - - return message; - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/engine/processors/push-to-users.ts b/tdrive/backend/node/src/services/notifications/services/engine/processors/push-to-users.ts deleted file mode 100644 index ba502267..00000000 --- a/tdrive/backend/node/src/services/notifications/services/engine/processors/push-to-users.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { logger } from "../../../../../core/platform/framework"; -import { MobilePushNotifier } from "../../../notifiers"; -import { - MentionNotification, - MentionNotificationResult, - NotificationMessageQueueHandler, - PushNotificationMessage, -} from "../../../types"; -import { ChannelMemberNotificationPreference } from "../../../entities"; -import { UserNotificationBadge } from "../../../entities"; -import _ from "lodash"; -import { websocketEventBus } from "../../../../../core/platform/services/realtime/bus"; -import { - RealtimeEntityActionType, - ResourcePath, -} from "../../../../../core/platform/services/realtime/types"; -import { getNotificationRoomName } from "../../realtime"; -import gr from "../../../../global-resolver"; -import { ExecutionContext } from "../../../../../core/platform/framework/api/crud-service"; - -/** - * Push new message notification to a set of users - */ -export class PushNotificationToUsersMessageProcessor - implements NotificationMessageQueueHandler -{ - readonly topics = { - in: "notification:mentions", - }; - - readonly options = { - unique: true, - ack: true, - }; - - name = "PushNotificationToUsersMessageProcessor"; - - validate(message: MentionNotification): boolean { - return !!( - message && - message.channel_id && - message.company_id && - message.workspace_id && - message.creation_date - ); - } - - async process( - message: MentionNotification, - context?: ExecutionContext, - ): Promise { - logger.info(`${this.name} - Processing mention notification for channel ${message.channel_id}`); - - if ( - !message.company_id || - !message.workspace_id || - !message.channel_id || - !message.creation_date - ) { - throw new Error("Missing required fields"); - } - - if (!message.mentions || !message.mentions.users || !message.mentions.users.length) { - logger.info(`${this.name} - Message does not have any user to mention`); - return; - } - - const usersToUpdate = await this.filterUsersOnLastReadChannelTime( - { channel_id: message.channel_id, company_id: message.company_id }, - message.mentions.users, - message.creation_date, - context, - ); - - if (!usersToUpdate.length) { - logger.info(`${this.name} - There are no users to notify from the last read channel date`); - return; - } - - const badges = await this.addNewMessageBadgesForUsers( - { - channel_id: message.channel_id, - company_id: message.company_id, - workspace_id: message.workspace_id, - thread_id: message.thread_id || message.message_id, - message_id: message.message_id, - }, - usersToUpdate, - message.mentions, - context, - ); - - badges.forEach(badge => { - const badgeLocation = { - company_id: message.company_id, - workspace_id: message.workspace_id, - channel_id: message.channel_id, - user: badge.user_id.toString(), - thread_id: message.thread_id || message.message_id, - message_id: message.message_id, - }; - - websocketEventBus.publish(RealtimeEntityActionType.Event, { - type: "notification:desktop", - room: ResourcePath.get(getNotificationRoomName(badge.user_id)), - entity: { - ...badgeLocation, - title: message.title, - text: formatNotificationMessage(message), - }, - resourcePath: null, - result: null, - }); - - this.sendPushNotification(badge.user_id, { - ...badgeLocation, - badge_value: 1, - title: message.title, - text: formatNotificationMessage(message), - }); - }); - } - - async filterUsersOnLastReadChannelTime( - channel: Pick, - users: string[] = [], - timestamp: number, - context?: ExecutionContext, - ): Promise { - if (!users.length) { - return []; - } - - return ( - await gr.services.notifications.channelPreferences.getChannelPreferencesForUsers( - channel, - users, - { - lessThan: timestamp, - }, - context, - ) - ) - .getEntities() - .map((preference: ChannelMemberNotificationPreference) => preference.user_id); - } - - async addNewMessageBadgesForUsers( - badge: Pick< - UserNotificationBadge, - "channel_id" | "company_id" | "thread_id" | "workspace_id" | "message_id" - >, - users: string[] = [], - mentions: MentionNotification["mentions"], - context: ExecutionContext, - ): Promise> { - logger.info(`${this.name} - Update badge for users ${users.join("/")}`); - - return ( - await Promise.all( - users.map(user => { - const badgeEntity = new UserNotificationBadge(); - _.assign(badgeEntity, { - channel_id: badge.channel_id, - company_id: badge.company_id, - workspace_id: badge.workspace_id, - thread_id: badge.thread_id, - message_id: badge.message_id, - user_id: user, - mention_type: mentions.users.includes(user), - }); - return this.saveBadge(badgeEntity, context); - }), - ) - ).filter(Boolean); - } - - private saveBadge( - badge: UserNotificationBadge, - context: ExecutionContext, - ): Promise { - return gr.services.notifications.badges - .save(badge, context) - .then(result => result.entity) - .catch(err => { - logger.warn({ err }, `${this.name} - A badge has not been saved for user ${badge.user_id}`); - return null; - }); - } - - sendPushNotification(user: string, pushNotification: PushNotificationMessage): void { - MobilePushNotifier.get(gr.platformServices.messageQueue).notify(user, pushNotification); - } -} - -function formatNotificationMessage(message: MentionNotification): string { - let text = message.text; - // Clean the message text to remove @userName:id-id-id - ((text || "").match(/@[^: ]+:([0-f-]{36})/gm) || []).forEach(match => { - const string = (match || "").trim(); - const id = string.split(":").pop(); - const fallback = string.split(":").shift(); - text = text.replace(string, message.object_names?.users[id] || fallback); - }); - // Clean the message text to remove #channelName:id-id-id - ((text || "").match(/#[^: ]+:([0-f-]{36})/gm) || []).forEach(match => { - const string = (match || "").trim(); - const id = string.split(":").pop(); - const fallback = string.split(":").shift(); - text = text.replace(string, message.object_names?.channels[id] || fallback); - }); - return text; -} diff --git a/tdrive/backend/node/src/services/notifications/services/engine/processors/reaction-notification.ts b/tdrive/backend/node/src/services/notifications/services/engine/processors/reaction-notification.ts deleted file mode 100644 index a231b60a..00000000 --- a/tdrive/backend/node/src/services/notifications/services/engine/processors/reaction-notification.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { - NotificationMessageQueueHandler, - PushNotificationMessage, - ReactionNotification, -} from "../../../types"; -import { logger } from "../../../../../core/platform/framework"; -import { websocketEventBus } from "../../../../../core/platform/services/realtime/bus"; -import { - RealtimeEntityActionType, - ResourcePath, -} from "../../../../../core/platform/services/realtime/types"; -import { MobilePushNotifier } from "../../../notifiers"; -import gr from "../../../../global-resolver"; -import { getNotificationRoomName } from "../../realtime"; -import { Channel } from "../../../../channels/entities/channel"; -import User from "../../../../user/entities/user"; -export class PushReactionNotification - implements NotificationMessageQueueHandler -{ - readonly topics = { - in: "notification:reaction", - }; - - readonly options = { - unique: true, - ack: true, - }; - - name = "PushReactionToUsersMessageProcessor"; - - validate(message: ReactionNotification): boolean { - return !!( - message && - message.message_id && - message.channel_id && - message.company_id && - message.workspace_id && - message.thread_id && - message.user_id && - message.reaction && - message.creation_date && - message.reaction_user_id - ); - } - - async process(task: ReactionNotification): Promise { - logger.info(`${this.name} - Processing reaction notification for message ${task.message_id}`); - - const location = { - company_id: task.company_id, - workspace_id: task.workspace_id, - channel_id: task.channel_id, - user: task.user_id.toString(), - thread_id: task.thread_id || task.message_id, - message_id: task.message_id, - }; - - const { title, text } = await this.buildNotificationMessageContent(task); - - websocketEventBus.publish(RealtimeEntityActionType.Event, { - type: "notification:desktop", - room: ResourcePath.get(getNotificationRoomName(task.user_id)), - entity: { - ...location, - title, - text, - }, - resourcePath: null, - result: null, - }); - - this.sendPushNotification(task.user_id, { - ...location, - title, - text, - }); - } - - sendPushNotification(user: string, reaction: PushNotificationMessage): void { - MobilePushNotifier.get(gr.platformServices.messageQueue).notify(user, reaction); - } - - async buildNotificationMessageContent( - message: ReactionNotification, - ): Promise<{ title: string; text: string }> { - const { company_id, workspace_id, reaction_user_id, reaction } = message; - let title = ""; - - const channel: Channel = await gr.services.channels.channels.get({ - id: message.channel_id, - company_id: company_id, - workspace_id: workspace_id, - }); - - const [company, workspace, user] = await Promise.all([ - gr.services.companies.getCompany({ id: company_id }), - workspace_id === "direct" - ? null - : gr.services.workspaces.get({ - company_id: company_id, - id: workspace_id, - }), - gr.services.users.get({ id: reaction_user_id }), - ]); - - const companyName = company?.name || ""; - const workspaceName = workspace_id === "direct" ? "Direct" : workspace?.name || ""; - const userName = this.getUserName(user) || "Tdrive"; - - if (Channel.isDirectChannel(channel)) { - title = `${userName} in ${companyName}`; - } else { - title = `${channel.name} in ${companyName} • ${workspaceName}`; - } - - return { - title, - text: `${userName}: ${reaction}`, - }; - } - - getUserName(user: User): string { - return ( - `${user?.first_name || ""} ${user?.last_name || ""}`.trim() || `@${user?.username_canonical}` - ); - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/mobile-push.ts b/tdrive/backend/node/src/services/notifications/services/mobile-push.ts deleted file mode 100644 index 9c08810e..00000000 --- a/tdrive/backend/node/src/services/notifications/services/mobile-push.ts +++ /dev/null @@ -1,59 +0,0 @@ -import Repository from "../../../core/platform/services/database/services/orm/repository/repository"; -import { PushNotificationMessage } from "../types"; -import User, { TYPE as UserType } from "../../user/entities/user"; -import { CrudException, ExecutionContext } from "../../../core/platform/framework/api/crud-service"; -import gr from "../../global-resolver"; - -export class MobilePushService { - name: "MobilePushService"; - version: "1"; - userRepository: Repository; - - async init(): Promise { - this.userRepository = await gr.database.getRepository(UserType, User); - return this; - } - - //Fixme: add a bulk system to group requests to fcm - async push(message: PushNotificationMessage, context?: ExecutionContext): Promise { - // Get devices and loop over devices - const userId = message.user; - - const user = await this.userRepository.findOne({ id: userId }, {}, context); - - if (!user) { - throw CrudException.notFound(`User ${userId} not found`); - } - - const notification = { - title: message.title, - body: message.text, - sound: "default", - badge: message.badge_value, - }; - - const options = { - notification_data: { - company_id: message.company_id, - workspace_id: message.workspace_id, - channel_id: message.channel_id, - message_id: message.message_id, - thread_id: message.thread_id, - }, - collapse_key: message.channel_id, - }; - - const { preferences } = await gr.services.notifications.preferences.getMerged( - { user_id: user.id, company_id: message.company_id, workspace_id: message.workspace_id }, - user, - ); - if (preferences.mobile_notifications === "never") { - return; - } - if (preferences.private_message_content) { - notification.body = "[Private]"; - } - - await gr.platformServices.push.push(user.devices, notification, options); - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/preferences.ts b/tdrive/backend/node/src/services/notifications/services/preferences.ts deleted file mode 100644 index 2002b121..00000000 --- a/tdrive/backend/node/src/services/notifications/services/preferences.ts +++ /dev/null @@ -1,147 +0,0 @@ -import _ from "lodash"; -import { Initializable, TdriveServiceProvider } from "../../../core/platform/framework"; -import { - CrudException, - DeleteResult, - ExecutionContext, - ListResult, - OperationType, - SaveResult, -} from "../../../core/platform/framework/api/crud-service"; -import Repository from "../../../core/platform/services/database/services/orm/repository/repository"; -import User, { UserNotificationPreferences } from "../../../services/user/entities/user"; -import gr from "../../global-resolver"; - -type UserNotificationPreferencesPrimaryKey = { - company_id: string; - workspace_id: string; - user_id: string; -}; - -export class NotificationPreferencesService implements TdriveServiceProvider, Initializable { - version: "1"; - repository: Repository; - - async init(): Promise { - return this; - } - - async list(): Promise> { - throw new Error("Not implemented"); - } - - async get( - pk: UserNotificationPreferencesPrimaryKey, - user?: User, - ): Promise { - user = user || (await gr.services.users.get({ id: pk.user_id })); - const notificationPreferences = (user?.preferences?.notifications || []).find(n => { - return n.company_id === pk.company_id && n.workspace_id === pk.workspace_id; - }); - return ( - notificationPreferences || { - company_id: pk.company_id, - workspace_id: pk.workspace_id, - preferences: { - highlight_words: [], - night_break: { - enable: false, - from: 0, - to: 0, - }, - private_message_content: false, - mobile_notifications: "always", - email_notifications_delay: 15, - deactivate_notifications_until: 0, - notification_sound: "default", - }, - } - ); - } - - /** We can define preferences for specifically a workspace or for all a company or all Tdrive - * This function will ensure we get all with inherit and all - */ - async getMerged( - pk: UserNotificationPreferencesPrimaryKey, - user?: User, - ): Promise { - let preferences = await this.get(pk, user); - if (pk.workspace_id !== "all") - preferences = _.merge(await this.get({ ...pk, workspace_id: "all" }), preferences); - if (pk.company_id !== "all") - preferences = _.merge( - await this.get({ ...pk, workspace_id: "all", company_id: "all" }), - preferences, - ); - return preferences; - } - - async delete( - pk: UserNotificationPreferencesPrimaryKey, - context?: ExecutionContext, - ): Promise> { - const user = await gr.services.users.get({ id: pk.user_id }); - const notificationPreferences = (user?.preferences?.notifications || []).filter(n => { - return n.company_id !== pk.company_id || n.workspace_id !== pk.workspace_id; - }); - - await gr.services.users.setPreferences( - { id: pk.user_id }, - { - notifications: notificationPreferences, - }, - context, - ); - - return new DeleteResult("user_notifications_preference", pk as any, true); - } - - async listPreferences( - workspace_id: string, - company_id: string, - user_id: string, - ): Promise> { - if (!workspace_id || !company_id || !user_id) { - throw CrudException.badRequest("workspace_id, company_id and user_id are required"); - } - - const user = await gr.services.users.get({ id: user_id }); - - const notificationPreferences = (user?.preferences?.notifications || []).filter(n => { - return n.company_id === company_id && n.workspace_id === workspace_id; - }); - - return new ListResult("user_notifications_preference", notificationPreferences); - } - - async savePreferences( - singleNotificationPreferences: UserNotificationPreferences, - userId: string, - context: ExecutionContext, - ): Promise> { - const user = await gr.services.users.get({ id: userId }); - - const notificationPreferences = (user?.preferences?.notifications || []).filter(n => { - return ( - n.company_id !== singleNotificationPreferences.company_id || - n.workspace_id !== singleNotificationPreferences.workspace_id - ); - }); - notificationPreferences.push(singleNotificationPreferences); - - await gr.services.users.setPreferences( - { id: userId }, - { - notifications: notificationPreferences, - }, - context, - ); - - return new SaveResult( - "user_notifications_preference", - singleNotificationPreferences, - OperationType.CREATE, - ); - } -} diff --git a/tdrive/backend/node/src/services/notifications/services/realtime.ts b/tdrive/backend/node/src/services/notifications/services/realtime.ts deleted file mode 100644 index b71d949e..00000000 --- a/tdrive/backend/node/src/services/notifications/services/realtime.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { User, WebsocketMetadata } from "../../../utils/types"; - -export function getWebsocketInformation(user: User): WebsocketMetadata { - return { - room: getNotificationRoomName(user.id), - token: "", - }; -} - -export function getNotificationRoomName(userId: string): string { - return `/notifications?type=private&user=${userId}`; -} diff --git a/tdrive/backend/node/src/services/notifications/types.ts b/tdrive/backend/node/src/services/notifications/types.ts deleted file mode 100644 index 2d7bd4ec..00000000 --- a/tdrive/backend/node/src/services/notifications/types.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { ExecutionContext } from "../../core/platform/framework/api/crud-service"; -import { Channel, ChannelMember } from "../channels/entities"; -import { PaginationQueryParameters } from "../channels/web/types"; -import { uuid } from "../../utils/types"; -import { MessageQueueHandler } from "../../core/platform/services/message-queue/api"; - -export type NotificationConfiguration = { - push: { - type: "fcm"; - fcm: { - endpoint: string; - key: string; - }; - }; -}; - -export type NotificationExecutionContext = ExecutionContext; - -export type MentionNotification = { - company_id: uuid; - workspace_id: uuid | "direct"; - channel_id: uuid; - thread_id: uuid; - message_id: uuid; - creation_date: number; - mentions?: { - users: uuid[]; - }; - object_names?: { - users: { [id: string]: string }; - channels: { [id: string]: string }; - }; - title: string; - text: string; -}; - -export type MentionNotificationResult = MentionNotification; - -export type ChannelReadMessage = { member: ChannelMember; channel: Channel }; -export type ChannelUnreadMessage = ChannelReadMessage; - -export type PushNotificationMessage = { - company_id: uuid; - workspace_id: uuid | "direct"; - channel_id: uuid; - message_id: uuid; - thread_id: uuid; - badge_value?: number; - user: string; - title: string; - text: string; -}; -export type PushNotificationMessageResult = PushNotificationMessage; - -export interface NotificationListQueryParameters extends PaginationQueryParameters { - company_id: uuid; - workspace_id: uuid | "direct"; - channel_id: uuid; - thread_id: uuid; - all_companies: boolean; -} - -export interface NotificationListUrlParameters { - company_id: uuid; -} - -export interface NotificationPreferenceListQueryParameters extends PaginationQueryParameters { - workspace_id: uuid | "all"; - company_id: uuid | "all"; - user_id: uuid; -} - -export type NotificationMessageQueueHandler = MessageQueueHandler< - InputMessage, - OutputMessage ->; - -export type ReactionNotification = { - company_id: uuid; - workspace_id: uuid | "direct"; - channel_id: uuid; - thread_id: uuid; - message_id: uuid; - creation_date: number; - user_id: string; - reaction: string; - reaction_user_id: string; -}; - -export type ReactionNotificationResult = ReactionNotification; - -export type NotificationAcknowledgeBody = { - thread_id: uuid; - workspace_id: uuid | "direct"; - channel_id: uuid; - message_id: uuid; -}; diff --git a/tdrive/backend/node/src/services/notifications/web/controllers/index.ts b/tdrive/backend/node/src/services/notifications/web/controllers/index.ts deleted file mode 100644 index 3d6bbf8f..00000000 --- a/tdrive/backend/node/src/services/notifications/web/controllers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./notification"; -export * from "./preferences"; diff --git a/tdrive/backend/node/src/services/notifications/web/controllers/notification.ts b/tdrive/backend/node/src/services/notifications/web/controllers/notification.ts deleted file mode 100644 index c4416898..00000000 --- a/tdrive/backend/node/src/services/notifications/web/controllers/notification.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { FastifyRequest } from "fastify"; -import { CrudController } from "../../../../core/platform/services/webserver/types"; -import { NotificationListQueryParameters } from "../../types"; -import { - ResourceCreateResponse, - ResourceDeleteResponse, - ResourceGetResponse, - ResourceListResponse, -} from "../../../../utils/types"; -import { UserNotificationBadge } from "../../entities"; -import { getWebsocketInformation } from "../../services/realtime"; -import gr from "../../../global-resolver"; -import { WorkspaceExecutionContext } from "../../../channels/types"; - -export class NotificationController - implements - CrudController< - ResourceGetResponse, - ResourceCreateResponse, - ResourceListResponse, - ResourceDeleteResponse - > -{ - async list( - request: FastifyRequest<{ - Querystring: NotificationListQueryParameters; - }>, - ): Promise> { - const context = getExecutionContext(request); - - let resources: UserNotificationBadge[] = []; - let page_token = ""; - - //Get one badge per company if requested - if (request.query.all_companies) { - const list = await gr.services.notifications.badges.listForUserPerCompanies( - request.currentUser.id, - context, - ); - resources = resources.concat(list.getEntities()); - } - - if (request.query.company_id) { - const list = await gr.services.notifications.badges.listForUser( - request.query.company_id, - request.currentUser.id, - { ...request.query }, - context, - ); - resources = resources.concat(list.getEntities()); - page_token = list.page_token; - } - - return { - ...{ - resources, - }, - ...(request.query.websockets && { - websockets: gr.platformServices.realtime.sign( - [getWebsocketInformation(request.currentUser)], - request.currentUser.id, - ), - }), - ...(page_token && { - next_page_token: page_token, - }), - }; - } -} - -function getExecutionContext(request: FastifyRequest): WorkspaceExecutionContext { - return { - user: request.currentUser, - url: request.url, - method: request.routerMethod, - reqId: request.id, - transport: "http", - workspace: { - company_id: undefined, - workspace_id: undefined, - }, - }; -} diff --git a/tdrive/backend/node/src/services/notifications/web/controllers/preferences.ts b/tdrive/backend/node/src/services/notifications/web/controllers/preferences.ts deleted file mode 100644 index 5114d78c..00000000 --- a/tdrive/backend/node/src/services/notifications/web/controllers/preferences.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { FastifyReply, FastifyRequest } from "fastify"; -import { ExecutionContext } from "../../../../core/platform/framework/api/crud-service"; -import { UserNotificationPreferences } from "../../../../services/user/entities/user"; -import { handleError } from "../../../../utils/handleError"; -import { ResourceCreateResponse, ResourceListResponse } from "../../../../utils/types"; -import gr from "../../../global-resolver"; -import { NotificationPreferenceListQueryParameters } from "../../types"; - -const ALL = "all"; - -export class NotificationPreferencesController { - async list( - request: FastifyRequest<{ - Querystring: NotificationPreferenceListQueryParameters; - }>, - ): Promise> { - const list = await gr.services.notifications.preferences.listPreferences( - ALL, - ALL, - request.currentUser.id, - ); - - let resources = list.getEntities(); - - if (!resources.length) { - resources = [ - { - company_id: ALL, - workspace_id: ALL, - preferences: { - highlight_words: [], - night_break: { enable: false, from: 0, to: 0 }, - private_message_content: false, - mobile_notifications: "always", - email_notifications_delay: 15, - deactivate_notifications_until: 0, - notification_sound: "default", - }, - }, - ]; - } - - return { resources }; - } - - async save( - request: FastifyRequest<{ Body: { resource: UserNotificationPreferences } }>, - reply: FastifyReply, - context?: ExecutionContext, - ): Promise> { - const entity = { - workspace_id: ALL, - company_id: ALL, - user_id: request.currentUser.id, - preferences: { - ...request.body.resource, - }, - }; - - try { - const result = await gr.services.notifications.preferences.savePreferences( - entity as UserNotificationPreferences, - request.currentUser.id, - context, - ); - - if (result) { - reply.code(201); - } - - return { - resource: result.entity, - }; - } catch (err) { - handleError(reply, err); - } - } -} diff --git a/tdrive/backend/node/src/services/notifications/web/index.ts b/tdrive/backend/node/src/services/notifications/web/index.ts deleted file mode 100644 index cef68280..00000000 --- a/tdrive/backend/node/src/services/notifications/web/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { FastifyInstance, FastifyRegisterOptions } from "fastify"; -import routes from "./routes"; - -export default ( - fastify: FastifyInstance, - options: FastifyRegisterOptions<{ - prefix: string; - }>, -): void => { - fastify.log.debug("Configuring /internal/services/notifications/v1 routes"); - fastify.register(routes, options); -}; diff --git a/tdrive/backend/node/src/services/notifications/web/middleware.ts b/tdrive/backend/node/src/services/notifications/web/middleware.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/tdrive/backend/node/src/services/notifications/web/routes.ts b/tdrive/backend/node/src/services/notifications/web/routes.ts deleted file mode 100644 index 29585943..00000000 --- a/tdrive/backend/node/src/services/notifications/web/routes.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { FastifyInstance, FastifyPluginCallback } from "fastify"; -import { NotificationController, NotificationPreferencesController } from "./controllers"; -import { createNotificationPreferencesSchema } from "./schemas"; - -const badgesUrl = "/badges"; -const notificationPreferencesUrl = "/preferences"; - -const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next) => { - const notificationController = new NotificationController(); - const notificationPreferencesController = new NotificationPreferencesController(); - - fastify.route({ - method: "GET", - url: badgesUrl, - preValidation: [fastify.authenticate], - handler: notificationController.list.bind(notificationController), - }); - - fastify.route({ - method: "GET", - url: notificationPreferencesUrl, - preValidation: [fastify.authenticate], - handler: notificationPreferencesController.list.bind(notificationPreferencesController), - }); - - fastify.route({ - method: "POST", - url: notificationPreferencesUrl, - preValidation: [fastify.authenticate], - schema: createNotificationPreferencesSchema, - handler: notificationPreferencesController.save.bind(notificationPreferencesController), - }); - - next(); -}; - -export default routes; diff --git a/tdrive/backend/node/src/services/notifications/web/schemas.ts b/tdrive/backend/node/src/services/notifications/web/schemas.ts deleted file mode 100644 index 40517676..00000000 --- a/tdrive/backend/node/src/services/notifications/web/schemas.ts +++ /dev/null @@ -1,43 +0,0 @@ -const notificationPreferencesSchema = { - type: "object", - properties: { - preferences: { - type: "object", - properties: { - highlight_words: { type: "array" }, - night_break: { - type: "object", - properties: { - enable: { type: "boolean" }, - from: { type: "number" }, - to: { type: "number" }, - }, - }, - private_message_content: { type: "boolean" }, - mobile_notifications: { type: "string" }, - email_notifications_delay: { type: "number" }, - deactivate_notifications_until: { type: "number" }, - notification_sound: { type: "string" }, - }, - }, - }, -}; - -export const createNotificationPreferencesSchema = { - body: { - type: "object", - properties: { - resource: notificationPreferencesSchema, - }, - required: ["resource"], - }, - response: { - 201: { - type: "object", - properties: { - resource: notificationPreferencesSchema, - }, - required: ["resource"], - }, - }, -}; diff --git a/tdrive/backend/node/src/services/notifications/web/types.ts b/tdrive/backend/node/src/services/notifications/web/types.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/tdrive/backend/node/src/services/user/services/companies.ts b/tdrive/backend/node/src/services/user/services/companies.ts index 24693f8a..94d2a78e 100644 --- a/tdrive/backend/node/src/services/user/services/companies.ts +++ b/tdrive/backend/node/src/services/user/services/companies.ts @@ -34,10 +34,6 @@ import { logger, RealtimeSaved } from "../../../core/platform/framework"; import { getCompanyRoom, getUserRoom } from "../realtime"; import gr from "../../global-resolver"; import { localEventBus } from "../../../core/platform/framework/event-bus"; -import { - KnowledgeGraphEvents, - KnowledgeGraphGenericEventPayload, -} from "../../../core/platform/services/knowledge-graph/types"; export class CompanyServiceImpl { version: "1"; @@ -100,15 +96,6 @@ export class CompanyServiceImpl { await this.externalCompanyRepository.save(extCompany, context); } - localEventBus.publish>( - KnowledgeGraphEvents.COMPANY_UPSERT, - { - id: company.id, - resource: company, - links: [], - }, - ); - return new SaveResult("company", company, OperationType.UPDATE); } diff --git a/tdrive/backend/node/src/services/user/services/users/service.ts b/tdrive/backend/node/src/services/user/services/users/service.ts index 2faba390..69b2e9fe 100644 --- a/tdrive/backend/node/src/services/user/services/users/service.ts +++ b/tdrive/backend/node/src/services/user/services/users/service.ts @@ -32,10 +32,6 @@ import { RealtimeSaved } from "../../../../core/platform/framework"; import { getPublicUserRoom, getUserRoom } from "../../realtime"; import NodeCache from "node-cache"; import gr from "../../../global-resolver"; -import { - KnowledgeGraphEvents, - KnowledgeGraphGenericEventPayload, -} from "../../../../core/platform/services/knowledge-graph/types"; import { formatUser } from "../../../../utils/users"; export class UserServiceImpl { @@ -121,27 +117,11 @@ export class UserServiceImpl { }, ]; }) - async save(user: User, context?: ExecutionContext): Promise> { + async save(user: User, context?: ExecutionContext): Promise> { this.assignDefaults(user); await this.repository.save(user, context); await this.updateExtRepository(user); - localEventBus.publish>( - KnowledgeGraphEvents.USER_UPSERT, - { - id: user.id, - resource: user, - links: [ - { - // FIXME: We should provide the company id here - id: "", - relation: "parent", - type: "company", - }, - ], - }, - ); - await this.publishPublicUserRealtime(user.id); return new SaveResult("user", user, OperationType.UPDATE); diff --git a/tdrive/backend/node/src/services/user/web/controller.ts b/tdrive/backend/node/src/services/user/web/controller.ts index 5900271e..d202cf2c 100644 --- a/tdrive/backend/node/src/services/user/web/controller.ts +++ b/tdrive/backend/node/src/services/user/web/controller.ts @@ -35,7 +35,6 @@ import { getCompanyRooms, getUserRooms } from "../realtime"; import { formatCompany, getCompanyStats } from "../utils"; import { formatUser } from "../../../utils/users"; import gr from "../../global-resolver"; -import { UserChannel, UsersIncludedChannel } from "../../channels/entities"; export class UsersCrudController implements @@ -282,39 +281,11 @@ export class UsersCrudController } async recent( - request: FastifyRequest<{ Params: CompanyParameters; Querystring: { limit: 100 } }>, + _request: FastifyRequest<{ Params: CompanyParameters; Querystring: { limit: 100 } }>, _reply: FastifyReply, ): Promise> { - const context = getExecutionContext(request); - const userId = context.user.id; - const companyId = request.params.id; - - let channels: UserChannel[] = await gr.services.channels.channels - .getChannelsForUsersInWorkspace(companyId, "direct", userId, undefined, context) - .then(list => list.getEntities()); - - channels = channels.sort((a, b) => b.last_activity - a.last_activity); - channels = channels.slice(0, 100); - - const userIncludedChannels: UsersIncludedChannel[] = await Promise.all( - channels.map( - channel => - gr.services.channels.channels.includeUsersInDirectChannel( - channel, - userId, - ) as Promise, - ), - ); - - const users: UserObject[] = []; - for (const channel of userIncludedChannels) { - for (const user of channel.users) { - if (user.id != request.currentUser.id) users.push(user); - } - } - return { - resources: [...uniq(users).slice(0, request.query.limit)], + resources: [], }; } } diff --git a/tdrive/backend/node/src/services/workspaces/services/workspace.ts b/tdrive/backend/node/src/services/workspaces/services/workspace.ts index 78cbaf16..6333d3aa 100644 --- a/tdrive/backend/node/src/services/workspaces/services/workspace.ts +++ b/tdrive/backend/node/src/services/workspaces/services/workspace.ts @@ -60,10 +60,6 @@ import { expandUUID4, reduceUUID4 } from "../../../utils/uuid-reducer"; import gr from "../../global-resolver"; import { logger } from "@sentry/utils"; import { localEventBus } from "../../../core/platform/framework/event-bus"; -import { - KnowledgeGraphEvents, - KnowledgeGraphGenericEventPayload, -} from "../../../core/platform/services/knowledge-graph/types"; import WorkspaceInviteDomain, { TYPE as WorkspaceInviteDomainType, getInstance as getWorkspaceInviteDomainInstance, @@ -239,15 +235,6 @@ export class WorkspaceServiceImpl implements TdriveServiceProvider, Initializabl }); } - localEventBus.publish>( - KnowledgeGraphEvents.WORKSPACE_UPSERT, - { - id: workspace.id, - resource: workspace, - links: [{ relation: "parent", type: "company", id: workspace.company_id }], - }, - ); - return new SaveResult( TYPE, workspace, diff --git a/tdrive/backend/node/src/services/workspaces/web/controllers/workspace-invite-tokens.ts b/tdrive/backend/node/src/services/workspaces/web/controllers/workspace-invite-tokens.ts index 6a1214a3..a44a4e09 100644 --- a/tdrive/backend/node/src/services/workspaces/web/controllers/workspace-invite-tokens.ts +++ b/tdrive/backend/node/src/services/workspaces/web/controllers/workspace-invite-tokens.ts @@ -227,23 +227,6 @@ export class WorkspaceInviteTokensCrudController ); } - await gr.services.channelPendingEmail.proccessPendingEmails( - { - company_id, - user_id: userId, - workspace_id, - }, - { - company_id, - workspace_id, - }, - { - user: request.currentUser, - url: request.url, - method: request.routerMethod, - transport: "http", - }, - ); resource.company.id = company.id; resource.workspace.id = workspace.id; } diff --git a/tdrive/backend/node/src/services/workspaces/web/controllers/workspace-users.ts b/tdrive/backend/node/src/services/workspaces/web/controllers/workspace-users.ts index 5c7b14f6..c2245e8e 100644 --- a/tdrive/backend/node/src/services/workspaces/web/controllers/workspace-users.ts +++ b/tdrive/backend/node/src/services/workspaces/web/controllers/workspace-users.ts @@ -35,7 +35,6 @@ import WorkspacePendingUser from "../../entities/workspace_pending_users"; import { ConsoleCompany, CreateConsoleUser } from "../../../console/types"; import { hasCompanyAdminLevel } from "../../../../utils/company"; import gr from "../../../global-resolver"; -import { getChannelPendingEmailsInstance } from "../../../channels/entities"; export class WorkspaceUsersCrudController implements @@ -413,16 +412,6 @@ export class WorkspaceUsersCrudController invitation.role, invitation.company_role, ); - (request.body.channels || []).forEach(async channelId => { - const channelPendingEmail = getChannelPendingEmailsInstance({ - channel_id: channelId, - company_id: request.params.company_id, - email: invitation.email, - workspace_id: context.workspace_id, - }); - - await gr.services.channelPendingEmail.create(channelPendingEmail, context); - }); responses.push({ email: invitation.email, status: "ok" }); }; @@ -516,18 +505,6 @@ export class WorkspaceUsersCrudController await Promise.all( usersToProcessImmediately.map(user => { gr.services.console.processPendingUser(user); - gr.services.channelPendingEmail.proccessPendingEmails( - { - company_id: context.company_id, - user_id: user.id, - workspace_id: context.workspace_id, - }, - { - workspace_id: context.workspace_id, - company_id: context.company_id, - }, - context, - ); }), ); diff --git a/tdrive/backend/node/src/utils/types.ts b/tdrive/backend/node/src/utils/types.ts index 550086d3..1f36f642 100644 --- a/tdrive/backend/node/src/utils/types.ts +++ b/tdrive/backend/node/src/utils/types.ts @@ -3,13 +3,6 @@ */ import { WorkspacePrimaryKey } from "../services/workspaces/entities/workspace"; -import { - Channel as ChannelEntity, - ChannelMember, - ChannelPendingEmails, - ChannelTab, -} from "../services/channels/entities"; -import { ChannelParameters } from "../services/channels/web/types"; export type uuid = string; @@ -102,14 +95,9 @@ export interface ResourceWebsocket { export interface ResourceEventsPayload { user: User; - channel?: ChannelEntity; - channelParameters?: ChannelParameters; - guest?: ChannelPendingEmails; - member?: ChannelMember; actor?: User; - resourcesBefore?: (User | ChannelEntity | ChannelTab | ChannelMember)[]; - resourcesAfter?: (User | ChannelEntity | ChannelTab | ChannelMember)[]; - tab?: ChannelTab; + resourcesBefore?: User[]; + resourcesAfter?: User[]; company?: { id: string }; workspace?: WorkspacePrimaryKey; } diff --git a/tdrive/backend/node/test/e2e/tags/tags.spec.ts b/tdrive/backend/node/test/e2e/tags/tags.spec.ts new file mode 100644 index 00000000..1de57fe5 --- /dev/null +++ b/tdrive/backend/node/test/e2e/tags/tags.spec.ts @@ -0,0 +1,306 @@ +import "reflect-metadata"; +import { afterAll, beforeAll, describe, expect, it } from "@jest/globals"; +import { init, TestPlatform } from "../setup"; +import { TestDbService } from "../utils.prepare.db"; +import { Tag } from "../../../src/services/tags/entities/tags"; +import { deserialize } from "class-transformer"; +import { + ResourceCreateResponse, + ResourceGetResponse, + ResourceListResponse, +} from "../../../src/utils/types"; + +describe("The Tag feature", () => { + const url = "/internal/services/tags/v1"; + let platform: TestPlatform; + let testDbService: TestDbService; + const tagIds: string[] = []; + + beforeAll(async ends => { + platform = await init({ + services: ["webserver", "database", "storage", "message-queue", "tags"], + }); + testDbService = await TestDbService.getInstance(platform, true); + ends(); + }); + + afterAll(async done => { + for (let i = 0; i < tagIds.length; i++) { + for (let j = 0; j < tagIds.length; j++) { + if (tagIds[j] === tagIds[i] && j !== i) { + throw new Error("Tag are not unique"); + } + } + } + await platform?.tearDown(); + platform = null; + done(); + }); + + describe("Create tag", () => { + it("should 201 if creator is a company admin", async done => { + const user = await testDbService.createUser([testDbService.defaultWorkspace()], { + companyRole: "admin", + }); + + const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); + + for (let i = 0; i < 3; i++) { + const createTag = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${platform.workspace.company_id}/tags`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + payload: { + name: `test${i}`, + colour: `#00000${i}`, + }, + }); + + const tagResult: ResourceCreateResponse = deserialize( + ResourceCreateResponse, + createTag.body, + ); + expect(createTag.statusCode).toBe(201); + expect(tagResult.resource).toBeDefined(); + expect(tagResult.resource.name).toBe(`test${i}`); + expect(tagResult.resource.colour).toBe(`#00000${i}`); + expect(tagResult.resource.company_id).toBe(platform.workspace.company_id); + expect(tagResult.resource.tag_id).toBe(`test${i}`); + + const getTag = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${platform.workspace.company_id}/tags/${tagResult.resource.tag_id}`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + expect(getTag.statusCode).toBe(200); + + tagIds.push(tagResult.resource.tag_id); + } + done(); + }); + + it("should 401 if creator is not a company admin", async done => { + const user = await testDbService.createUser([testDbService.defaultWorkspace()], { + companyRole: "member", + }); + const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); + const createTag = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${platform.workspace.company_id}/tags`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + payload: { + name: "testNotAdmin", + colour: "#000000", + }, + }); + expect(createTag.statusCode).toBe(401); + + const tagResult: ResourceCreateResponse = deserialize( + ResourceCreateResponse, + createTag.body, + ); + expect(tagResult.resource).toBe(undefined); + done(); + }); + }); + + describe("Get tag", () => { + it("should 200 get a tag", async done => { + const user = await testDbService.createUser([testDbService.defaultWorkspace()], { + companyRole: "member", + }); + const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); + const getTag = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${platform.workspace.company_id}/tags/${tagIds[0]}`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + expect(getTag.statusCode).toBe(200); + + const getResult: ResourceGetResponse = deserialize(ResourceGetResponse, getTag.body); + expect(getResult.resource).toBeDefined(); + expect(getResult.resource.name).toBe("test0"); + expect(getResult.resource.colour).toBe("#000000"); + expect(getResult.resource.company_id).toBe(platform.workspace.company_id); + + done(); + }); + + it("should 200 tag does not exist", async done => { + const user = await testDbService.createUser([testDbService.defaultWorkspace()], { + companyRole: "member", + }); + const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); + const getTag = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${platform.workspace.company_id}/tags/NonExistingTag`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + expect(getTag.statusCode).toBe(200); + + const getResult: ResourceGetResponse = deserialize(ResourceGetResponse, getTag.body); + expect(getResult.resource).toBe(null); + + done(); + }); + }); + + describe("Update tag", () => { + it("Should 204 if user is admin", async done => { + const user = await testDbService.createUser([testDbService.defaultWorkspace()], { + companyRole: "admin", + }); + const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); + const updateTag = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${platform.workspace.company_id}/tags`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + payload: { + name: "test1", + colour: "#000003", + }, + }); + expect(updateTag.statusCode).toBe(201); + + const tagUpdatedResult: ResourceCreateResponse = deserialize( + ResourceCreateResponse, + updateTag.body, + ); + expect(tagUpdatedResult.resource).toBeDefined(); + expect(tagUpdatedResult.resource.name).toBe("test1"); + expect(tagUpdatedResult.resource.colour).toBe("#000003"); + expect(tagUpdatedResult.resource.company_id).toBe(platform.workspace.company_id); + expect(tagUpdatedResult.resource.tag_id).toBe("test1"); + + const getUpdatedTag = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${platform.workspace.company_id}/tags/${tagUpdatedResult.resource.tag_id}`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + expect(getUpdatedTag.statusCode).toBe(200); + + done(); + }); + + it("should 401 if creator is not a company admin", async done => { + const user = await testDbService.createUser([testDbService.defaultWorkspace()], { + companyRole: "member", + }); + const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); + const createTag = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${platform.workspace.company_id}/tags/testNotAdmin`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + payload: { + name: "testNotAdmin", + colour: "#000000", + }, + }); + expect(createTag.statusCode).toBe(401); + + const tagResult: ResourceCreateResponse = deserialize( + ResourceCreateResponse, + createTag.body, + ); + console.log("tagResult2", tagResult, jwtToken); + expect(tagResult.resource).toBe(undefined); + + done(); + }); + }); + + describe("List tags", () => { + it("should 200 list a tag", async done => { + const user = await testDbService.createUser([testDbService.defaultWorkspace()], { + companyRole: "member", + }); + const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); + const listTag = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${platform.workspace.company_id}/tags`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + expect(listTag.statusCode).toBe(200); + + const tagResult: ResourceListResponse = deserialize(ResourceListResponse, listTag.body); + expect(tagResult.resources).toBeDefined(); + for (const tag of tagResult.resources) { + expect(tag.name).toBeDefined(); + expect(tag.colour).toBeDefined(); + expect(tag.company_id).toBe(platform.workspace.company_id); + expect(tag.tag_id).toBeDefined(); + } + + done(); + }); + }); + + describe("Delete tag", () => { + it("should 200 if admin delete a tag", async done => { + const user = await testDbService.createUser([testDbService.defaultWorkspace()], { + companyRole: "admin", + }); + const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); + const deleteTag = await platform.app.inject({ + method: "DELETE", + url: `${url}/companies/${platform.workspace.company_id}/tags/${tagIds[0]}`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + expect(deleteTag.statusCode).toBe(200); + + done(); + }); + + it("should 200 if tag does not exist", async done => { + const user = await testDbService.createUser([testDbService.defaultWorkspace()], { + companyRole: "admin", + }); + const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); + const deleteTag = await platform.app.inject({ + method: "DELETE", + url: `${url}/companies/${platform.workspace.company_id}/tags/NonExistingTag`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + expect(deleteTag.statusCode).toBe(200); + done(); + }); + + it("should 401 if not admin", async done => { + const user = await testDbService.createUser([testDbService.defaultWorkspace()], { + companyRole: "member", + }); + const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); + const deleteTag = await platform.app.inject({ + method: "DELETE", + url: `${url}/companies/${platform.workspace.company_id}/tags/${tagIds[0]}`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + expect(deleteTag.statusCode).toBe(401); + done(); + }); + }); +}); diff --git a/tdrive/backend/node/test/unit/core/services/database/services/orm/connectors/cassandra/query-builder.test.ts b/tdrive/backend/node/test/unit/core/services/database/services/orm/connectors/cassandra/query-builder.test.ts index d365e899..de2cda54 100644 --- a/tdrive/backend/node/test/unit/core/services/database/services/orm/connectors/cassandra/query-builder.test.ts +++ b/tdrive/backend/node/test/unit/core/services/database/services/orm/connectors/cassandra/query-builder.test.ts @@ -5,79 +5,64 @@ import { buildComparison, buildIn, } from "../../../../../../../../../src/core/platform/services/database/services/orm/connectors/cassandra/query-builder"; -import { ChannelMemberNotificationPreference } from "../../../../../../../../../src/services/notifications/entities/channel-member-notification-preferences"; +import { DriveFile } from "../../../../../../../../../src/services/documents/entities/drive-file"; describe("The QueryBuilder module", () => { describe("The buildSelectQuery function", () => { it("should build a valid query from primary key parameters", () => { const filters = { company_id: "comp1", - channel_id: "chan1", + parent_id: "parent1", }; - const result = buildSelectQuery( - ChannelMemberNotificationPreference, - filters, - {}, - { keyspace: "tdrive" }, - ); + const result = buildSelectQuery(DriveFile, filters, {}, { keyspace: "tdrive" }); expect(result).toEqual( - "SELECT * FROM tdrive.channel_members_notification_preferences WHERE company_id = comp1 AND channel_id = chan1;", + "SELECT * FROM tdrive.drive_files WHERE company_id = comp1 AND parent_id = 'parent1';", ); }); it("should build a valid query from primary key parameters and comparison", () => { const filters = { company_id: "comp1", - channel_id: "chan1", + parent_id: "parent1", }; - const result = buildSelectQuery( - ChannelMemberNotificationPreference, + const result = buildSelectQuery( + DriveFile, filters, { - $lt: [["last_read", 1000]], + $lt: [["size", 1000]], }, { keyspace: "tdrive" }, ); expect(result).toEqual( - "SELECT * FROM tdrive.channel_members_notification_preferences WHERE company_id = comp1 AND channel_id = chan1 AND last_read < 1000;", + "SELECT * FROM tdrive.drive_files WHERE company_id = comp1 AND parent_id = 'parent1' AND size < 1000;", ); }); it("should build IN query from array parameters", () => { const filters = { company_id: "comp1", - channel_id: "chan1", - user_id: ["u1", "u2", "u3"], + parent_id: "parent1", + creator: ["u1", "u2", "u3"], }; - const result = buildSelectQuery( - ChannelMemberNotificationPreference, - filters, - {}, - { keyspace: "tdrive" }, - ); + const result = buildSelectQuery(DriveFile, filters, {}, { keyspace: "tdrive" }); expect(result).toEqual( - "SELECT * FROM tdrive.channel_members_notification_preferences WHERE company_id = comp1 AND channel_id = chan1 AND user_id IN (u1,u2,u3);", + "SELECT * FROM tdrive.drive_files WHERE company_id = comp1 AND parent_id = 'parent1' AND creator IN (u1,u2,u3);", ); }); it("should not build IN query from array parameters when array is empty", () => { const filters = { company_id: "comp1", - channel_id: "chan1", + parent_id: "parent1", user_id: [], }; - const result = buildSelectQuery( - ChannelMemberNotificationPreference, - filters, - {}, - { keyspace: "tdrive" }, - ); + const result = buildSelectQuery(DriveFile, filters, {}, { keyspace: "tdrive" }); expect(result).toEqual( - "SELECT * FROM tdrive.channel_members_notification_preferences WHERE company_id = comp1 AND channel_id = chan1;", + "SELECT * FROM tdrive.drive_files WHERE company_id = comp1 AND parent_id = 'parent1';", ); }); }); diff --git a/tdrive/default-configuration/backend-node/production.json b/tdrive/default-configuration/backend-node/production.json index e80f9365..d9de0a72 100644 --- a/tdrive/default-configuration/backend-node/production.json +++ b/tdrive/default-configuration/backend-node/production.json @@ -1,7 +1,4 @@ { - "phpnode": { - "secret": "api_supersecret" - }, "websocket": { "path": "/socket/", "adapters": { diff --git a/tdrive/default-configuration/backend-node/production.json.template b/tdrive/default-configuration/backend-node/production.json.template index d5583fe0..29a26450 100644 --- a/tdrive/default-configuration/backend-node/production.json.template +++ b/tdrive/default-configuration/backend-node/production.json.template @@ -1,7 +1,4 @@ { - "phpnode": { - "secret": "api_supersecret (set the same in php later)" - }, "websocket": { "path": "/socket/", "adapters": {