🧹 Propose to remove channels, notifications, knowledge-graph and phpnode (#55)
* Propose to remove channels and notifications * Propose to remove knowledge graph too * Put back tags test * FIxing tests * FIx query builder test
This commit is contained in:
@@ -24,9 +24,6 @@
|
|||||||
"root": "STATIC_ROOT"
|
"root": "STATIC_ROOT"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"phpnode": {
|
|
||||||
"secret": "PHP_NODE_API_SECRET"
|
|
||||||
},
|
|
||||||
"websocket": {
|
"websocket": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"jwt": {
|
"jwt": {
|
||||||
@@ -44,15 +41,6 @@
|
|||||||
"secret": "AUTH_JWT_SECRET"
|
"secret": "AUTH_JWT_SECRET"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
|
||||||
"push": {
|
|
||||||
"type": "PUSH_DRIVER",
|
|
||||||
"fcm": {
|
|
||||||
"endpoint": "PUSH_FCM_ENDPOINT",
|
|
||||||
"key": "PUSH_FCM_KEY"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"database": {
|
"database": {
|
||||||
"secret": "DB_SECRET",
|
"secret": "DB_SECRET",
|
||||||
"type": "DB_DRIVER",
|
"type": "DB_DRIVER",
|
||||||
|
|||||||
@@ -127,12 +127,6 @@
|
|||||||
"path": "/storage/"
|
"path": "/storage/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"knowledge-graph": {
|
|
||||||
"endpoint": "http://host-gateway:8888",
|
|
||||||
"callback_token": "secret",
|
|
||||||
"use": false,
|
|
||||||
"forwarded_companies": []
|
|
||||||
},
|
|
||||||
"email-pusher": {
|
"email-pusher": {
|
||||||
"endpoint": "https://api.smtp2go.com/v3/email/send",
|
"endpoint": "https://api.smtp2go.com/v3/email/send",
|
||||||
"api_key": "secret",
|
"api_key": "secret",
|
||||||
@@ -153,12 +147,9 @@
|
|||||||
"search",
|
"search",
|
||||||
"message-queue",
|
"message-queue",
|
||||||
"realtime",
|
"realtime",
|
||||||
"phpnode",
|
|
||||||
"tracker",
|
"tracker",
|
||||||
"general",
|
"general",
|
||||||
"user",
|
"user",
|
||||||
"channels",
|
|
||||||
"notifications",
|
|
||||||
"files",
|
"files",
|
||||||
"workspaces",
|
"workspaces",
|
||||||
"console",
|
"console",
|
||||||
@@ -166,8 +157,6 @@
|
|||||||
"statistics",
|
"statistics",
|
||||||
"cron",
|
"cron",
|
||||||
"online",
|
"online",
|
||||||
"knowledge-graph",
|
|
||||||
"knowledge-graph-web",
|
|
||||||
"email-pusher",
|
"email-pusher",
|
||||||
"documents",
|
"documents",
|
||||||
"applications",
|
"applications",
|
||||||
|
|||||||
@@ -19,8 +19,5 @@
|
|||||||
"amqp": {
|
"amqp": {
|
||||||
"urls": ["amqp://guest:guest@rabbitmq:5672"]
|
"urls": ["amqp://guest:guest@rabbitmq:5672"]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"phpnode": {
|
|
||||||
"php_endpoint": false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export interface authenticateDecorator {
|
|||||||
|
|
||||||
declare module "fastify" {
|
declare module "fastify" {
|
||||||
interface FastifyInstance {
|
interface FastifyInstance {
|
||||||
phpnodeAuthenticate(): void;
|
|
||||||
authenticate(): void;
|
authenticate(): void;
|
||||||
authenticateOptional(): void;
|
authenticateOptional(): void;
|
||||||
io: SocketIO.Server;
|
io: SocketIO.Server;
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import tdrive from "../../../tdrive";
|
|||||||
import { mkdirSync, writeFileSync } from "fs";
|
import { mkdirSync, writeFileSync } from "fs";
|
||||||
import { Pagination } from "../../../core/platform/framework/api/crud-service";
|
import { Pagination } from "../../../core/platform/framework/api/crud-service";
|
||||||
import WorkspaceUser from "../../../services/workspaces/entities/workspace_user";
|
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 { formatCompany } from "../../../services/user/utils";
|
||||||
import gr from "../../../services/global-resolver";
|
import gr from "../../../services/global-resolver";
|
||||||
import { formatUser } from "../../../utils/users";
|
import { formatUser } from "../../../utils/users";
|
||||||
@@ -105,91 +103,6 @@ const command: yargs.CommandModule<unknown, CLIArgs> = {
|
|||||||
}
|
}
|
||||||
writeFileSync(`${output}/users.json`, JSON.stringify(users));
|
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();
|
await platform.stop();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { DatabaseServiceAPI } from "../../../core/platform/services/database/api
|
|||||||
import { Pagination } from "../../../core/platform/framework/api/crud-service";
|
import { Pagination } from "../../../core/platform/framework/api/crud-service";
|
||||||
|
|
||||||
import User, { TYPE as UserTYPE } from "../../../services/user/entities/user";
|
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 Repository from "../../../core/platform/services/database/services/orm/repository/repository";
|
||||||
import { SearchServiceAPI } from "../../../core/platform/services/search/api";
|
import { SearchServiceAPI } from "../../../core/platform/services/search/api";
|
||||||
import CompanyUser, { TYPE as CompanyUserTYPE } from "../../../services/user/entities/company_user";
|
import CompanyUser, { TYPE as CompanyUserTYPE } from "../../../services/user/entities/company_user";
|
||||||
@@ -29,7 +28,6 @@ class SearchIndexAll {
|
|||||||
public async run(options: Options = {}): Promise<void> {
|
public async run(options: Options = {}): Promise<void> {
|
||||||
const repositories: Map<string, Repository<any>> = new Map();
|
const repositories: Map<string, Repository<any>> = new Map();
|
||||||
repositories.set("users", await this.database.getRepository(UserTYPE, User));
|
repositories.set("users", await this.database.getRepository(UserTYPE, User));
|
||||||
repositories.set("channels", await this.database.getRepository("channels", Channel));
|
|
||||||
|
|
||||||
const repository = repositories.get(options.repository);
|
const repository = repositories.get(options.repository);
|
||||||
if (!repository) {
|
if (!repository) {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Channel } from "../../../../services/channels/entities/channel";
|
|
||||||
import Company from "../../../../services/user/entities/company";
|
import Company from "../../../../services/user/entities/company";
|
||||||
import Workspace from "../../../../services/workspaces/entities/workspace";
|
import Workspace from "../../../../services/workspaces/entities/workspace";
|
||||||
import User from "../../../../services/user/entities/user";
|
import User from "../../../../services/user/entities/user";
|
||||||
@@ -7,7 +6,6 @@ export type EmailBuilderDataPayload = {
|
|||||||
user: User;
|
user: User;
|
||||||
company: Company;
|
company: Company;
|
||||||
notifications: {
|
notifications: {
|
||||||
channel: Channel;
|
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<Company>): Promise<void> {
|
|
||||||
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<Workspace>): Promise<void> {
|
|
||||||
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<User>): Promise<void> {
|
|
||||||
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<Channel>): Promise<void> {
|
|
||||||
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<KnowledgeGraphCreateMessageObjectData[]>
|
|
||||||
>(`${this.apiUrl}/topics/tdrive`, data, {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/vnd.kafka.json.v2+json",
|
|
||||||
Accept: "application/vnd.kafka.v2+json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<KnowledgeGraphAPI>
|
|
||||||
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<this> {
|
|
||||||
const use = this.getConfigurationEntry<boolean>("use");
|
|
||||||
|
|
||||||
if (!use) {
|
|
||||||
this.logger.warn("Knowledge graph is not used");
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
localEventBus.subscribe<KnowledgeGraphGenericEventPayload<Company>>(
|
|
||||||
KnowledgeGraphEvents.COMPANY_UPSERT,
|
|
||||||
this.onCompanyCreated.bind(this),
|
|
||||||
);
|
|
||||||
|
|
||||||
localEventBus.subscribe<KnowledgeGraphGenericEventPayload<Workspace>>(
|
|
||||||
KnowledgeGraphEvents.WORKSPACE_UPSERT,
|
|
||||||
this.onWorkspaceCreated.bind(this),
|
|
||||||
);
|
|
||||||
|
|
||||||
localEventBus.subscribe<KnowledgeGraphGenericEventPayload<Channel>>(
|
|
||||||
KnowledgeGraphEvents.CHANNEL_UPSERT,
|
|
||||||
this.onChannelCreated.bind(this),
|
|
||||||
);
|
|
||||||
|
|
||||||
localEventBus.subscribe<KnowledgeGraphGenericEventPayload<User>>(
|
|
||||||
KnowledgeGraphEvents.USER_UPSERT,
|
|
||||||
this.onUserCreated.bind(this),
|
|
||||||
);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** When the KG service send us new events */
|
|
||||||
async onCallbackEvent(token: string, data: KnowledgeGraphCallbackEvent): Promise<void> {
|
|
||||||
if (token === this.getConfigurationEntry<string>("callback_token")) {
|
|
||||||
this.logger.info("Unimplemented: KnowledgeGraph - Callback event", data);
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid token");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async onCompanyCreated(data: KnowledgeGraphGenericEventPayload<Company>): Promise<void> {
|
|
||||||
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<Workspace>): Promise<void> {
|
|
||||||
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<Channel>): Promise<void> {
|
|
||||||
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<User>): Promise<void> {
|
|
||||||
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<T>(key: string): T {
|
|
||||||
const configuration = new Configuration("knowledge-graph");
|
|
||||||
return configuration.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getKnowledgeGraphApiClient(): KnowledgeGraphAPIClient {
|
|
||||||
const endpoint = this.getConfigurationEntry<string>("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<false | "all" | "metadata"> {
|
|
||||||
const user = userId ? await gr.services.users.get({ id: userId }) : null;
|
|
||||||
const forwardedCompanies = this.getConfigurationEntry<string[]>("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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Company>): void;
|
|
||||||
onWorkspaceCreated(data: KnowledgeGraphGenericEventPayload<Workspace>): void;
|
|
||||||
onChannelCreated(data: KnowledgeGraphGenericEventPayload<Channel>): void;
|
|
||||||
onUserCreated(data: KnowledgeGraphGenericEventPayload<User>): void;
|
|
||||||
}
|
|
||||||
@@ -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<T> = {
|
|
||||||
records: T;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type KnowledgeGraphRelationLinkObject = {
|
|
||||||
relation: "mention" | "sender" | "parent" | "children" | "owner";
|
|
||||||
type: "user" | "channel" | "workspace" | "company" | "message";
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type KnowledgeGraphGenericEventPayload<T> = {
|
|
||||||
id: string;
|
|
||||||
resource: Partial<T>;
|
|
||||||
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;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -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<PhpNodeAPI> implements PhpNodeAPI {
|
|
||||||
name = "phpnode";
|
|
||||||
version = "1";
|
|
||||||
private server: FastifyInstance<Server, IncomingMessage, ServerResponse>;
|
|
||||||
private ws: WebSocketAPI;
|
|
||||||
private realtime: RealtimeServiceAPI;
|
|
||||||
|
|
||||||
api(): PhpNodeAPI {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async accessControl(
|
|
||||||
request: FastifyRequest,
|
|
||||||
server: FastifyInstance<Server, IncomingMessage, ServerResponse>,
|
|
||||||
) {
|
|
||||||
const token = (request.headers.authorization || "").trim().split("Token ").pop();
|
|
||||||
const secret = this.configuration.get<string>("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<this> {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async doInit(): Promise<this> {
|
|
||||||
this.server = this.context.getProvider<WebServerAPI>("webserver").getServer();
|
|
||||||
this.ws = this.context.getProvider<WebSocketAPI>("websocket");
|
|
||||||
this.realtime = this.context.getProvider<RealtimeServiceAPI>("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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Server, IncomingMessage, ServerResponse>,
|
|
||||||
): Promise<void>;
|
|
||||||
register(parameters: { method: string; url: string; handler: RouteHandlerMethod }): void;
|
|
||||||
}
|
|
||||||
@@ -43,49 +43,9 @@ export default class Tracker extends TdriveService<TrackerAPI> implements Tracke
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const channelCreatedEvent = "channel:created";
|
|
||||||
localEventBus.subscribe<ResourceEventsPayload>(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<ResourceEventsPayload>(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;
|
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(
|
public async identify(
|
||||||
identity: IdentifyObjectType,
|
identity: IdentifyObjectType,
|
||||||
callback?: (err: Error) => void,
|
callback?: (err: Error) => void,
|
||||||
|
|||||||
@@ -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<string, unknown>
|
|
||||||
| {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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",
|
|
||||||
}
|
|
||||||
@@ -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];
|
|
||||||
@@ -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>): 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>): MemberOfChannel {
|
|
||||||
return merge(new MemberOfChannel(), member);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -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>): Channel {
|
|
||||||
return merge(new Channel(), channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChannelPrimaryKey = Pick<Channel, "company_id" | "workspace_id" | "id">;
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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";
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { Type } from "class-transformer";
|
|
||||||
|
|
||||||
export class Member {
|
|
||||||
@Type(() => String)
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
>;
|
|
||||||
@@ -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<undefined> {
|
|
||||||
version = "1";
|
|
||||||
name = "channels";
|
|
||||||
|
|
||||||
public async doInit(): Promise<this> {
|
|
||||||
const fastify = this.context.getProvider<WebServerAPI>("webserver").getServer();
|
|
||||||
fastify.register((instance, _opts, next) => {
|
|
||||||
web(instance, { prefix: this.prefix });
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: remove
|
|
||||||
api(): undefined {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<this> {
|
|
||||||
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<ResourceEventsPayload>(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<ResourceEventsPayload>(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<ResourceEventsPayload>(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<ChannelEntity> = _.pick(data.resourcesBefore[0], [
|
|
||||||
"connectors",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const connectorsAfter: Partial<ChannelEntity> = _.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<ResourceEventsPayload>(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<ResourceEventsPayload>(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<ResourceEventsPayload>(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<ResourceEventsPayload>(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<void> {
|
|
||||||
logger.debug(`Activities - New ${event} event %o`, data);
|
|
||||||
try {
|
|
||||||
await this.messageQueue.publish<ActivityPublishedType>("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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -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<this> {
|
|
||||||
localEventBus.subscribe<ResourceEventsPayload>(
|
|
||||||
"channel:created",
|
|
||||||
this.onChannelCreated.bind(this),
|
|
||||||
);
|
|
||||||
localEventBus.subscribe<ResourceEventsPayload>(
|
|
||||||
"channel:updated",
|
|
||||||
this.onChannelUpdated.bind(this),
|
|
||||||
);
|
|
||||||
localEventBus.subscribe<ResourceEventsPayload>(
|
|
||||||
"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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<DefaultChannel>;
|
|
||||||
listener: DefaultChannelListener;
|
|
||||||
|
|
||||||
async init(): Promise<this> {
|
|
||||||
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<CreateResult<DefaultChannel>> {
|
|
||||||
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<DefaultChannel>("default_channel", channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
get(pk: DefaultChannelPrimaryKey, context: ExecutionContext): Promise<DefaultChannel> {
|
|
||||||
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<UpdateResult<DefaultChannel>> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
save?<SaveOptions>(
|
|
||||||
// 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<SaveResult<DefaultChannel>> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(
|
|
||||||
pk: DefaultChannelPrimaryKey,
|
|
||||||
context?: ExecutionContext,
|
|
||||||
): Promise<DeleteResult<DefaultChannel>> {
|
|
||||||
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<ListOptions>(
|
|
||||||
// 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<ListResult<DefaultChannel>> {
|
|
||||||
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<Pick<DefaultChannelPrimaryKey, "company_id" | "workspace_id">>,
|
|
||||||
): Promise<Array<{ channel: Channel; member?: ChannelMember; err?: Error; added: boolean }>> {
|
|
||||||
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<DefaultChannelPrimaryKey, "company_id" | "workspace_id">,
|
|
||||||
pagination?: Paginable,
|
|
||||||
context?: ExecutionContext,
|
|
||||||
): Promise<DefaultChannel[]> {
|
|
||||||
return this.getDefaultChannels$(workspace, pagination, context).pipe(toArray()).toPromise();
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultChannels$(
|
|
||||||
workspace: Pick<DefaultChannelPrimaryKey, "company_id" | "workspace_id">,
|
|
||||||
pagination?: Paginable,
|
|
||||||
context?: ExecutionContext,
|
|
||||||
): Observable<DefaultChannel> {
|
|
||||||
const list = (
|
|
||||||
workspace: Pick<DefaultChannelPrimaryKey, "company_id" | "workspace_id">,
|
|
||||||
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$);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ChannelPendingEmails>;
|
|
||||||
|
|
||||||
async init(): Promise<this> {
|
|
||||||
this.repository = await gr.database.getRepository(
|
|
||||||
"channel_pending_emails",
|
|
||||||
ChannelPendingEmails,
|
|
||||||
);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(
|
|
||||||
pendingEmail: ChannelPendingEmails,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<CreateResult<ChannelPendingEmails>> {
|
|
||||||
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<ChannelPendingEmails>("channel_pending_emails", pendingEmail);
|
|
||||||
}
|
|
||||||
|
|
||||||
get(
|
|
||||||
pk: ChannelPendingEmailsPrimaryKey,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<ChannelPendingEmails> {
|
|
||||||
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<UpdateResult<ChannelPendingEmails>> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
save?<SaveOptions>(
|
|
||||||
// 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<SaveResult<ChannelPendingEmails>> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(
|
|
||||||
pk: ChannelPendingEmailsPrimaryKey,
|
|
||||||
context?: ExecutionContext,
|
|
||||||
): Promise<DeleteResult<ChannelPendingEmails>> {
|
|
||||||
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<ListOptions>(
|
|
||||||
// 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<ListResult<ChannelPendingEmails>> {
|
|
||||||
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<ChannelPendingEmails>(
|
|
||||||
"channel_pending_emails",
|
|
||||||
result
|
|
||||||
.getEntities()
|
|
||||||
.map(pendingMember =>
|
|
||||||
plainToClass(ChannelPendingEmails, { email: pendingMember.email, ...pendingMember }),
|
|
||||||
),
|
|
||||||
result.nextPage,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
findPendingEmails(
|
|
||||||
pk: ChannelPendingEmailsListQueryParameters,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<ListResult<ChannelPendingEmails>> {
|
|
||||||
return this.repository.find(pk, {}, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
async proccessPendingEmails(
|
|
||||||
user: NewUserInWorkspaceNotification,
|
|
||||||
workspace: Required<Pick<ChannelPrimaryKey, "company_id" | "workspace_id">>,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<void> {
|
|
||||||
// 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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Channel, "id">,
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
@@ -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<ChannelActivity>;
|
|
||||||
channelRepository: Repository<Channel>;
|
|
||||||
directChannelRepository: Repository<DirectChannel>;
|
|
||||||
defaultChannelService: DefaultChannelServiceImpl;
|
|
||||||
|
|
||||||
async init(): Promise<this> {
|
|
||||||
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<ResourceEventsPayload>("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>((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<SaveResult<ChannelObject>> {
|
|
||||||
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<Record<keyof Channel, boolean>> = {
|
|
||||||
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<Partial<keyof Channel>>;
|
|
||||||
|
|
||||||
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<ResourceEventsPayload>("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<string>(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<ChannelObject>(
|
|
||||||
"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<ChannelObject> {
|
|
||||||
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>((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<UpdateResult<ChannelObject>> {
|
|
||||||
// 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<ChannelObject>("channel", ChannelObject.mapTo(mergeChannel));
|
|
||||||
}
|
|
||||||
|
|
||||||
@RealtimeDeleted<Channel>((channel, context) => [
|
|
||||||
{
|
|
||||||
room: ResourcePath.get(getRoomName(channel, context as WorkspaceExecutionContext)),
|
|
||||||
path: getChannelPath(channel, context as WorkspaceExecutionContext),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
async delete(
|
|
||||||
pk: ChannelPrimaryKey,
|
|
||||||
context: WorkspaceExecutionContext,
|
|
||||||
): Promise<DeleteResult<ChannelObject>> {
|
|
||||||
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<ResourceEventsPayload>("channel:deleted", {
|
|
||||||
channel: channelToDelete,
|
|
||||||
user: context.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@RealtimeUpdated<ChannelActivity>((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<UpdateResult<ChannelActivity>> {
|
|
||||||
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<KnowledgeGraphGenericEventPayload<Channel>>(
|
|
||||||
KnowledgeGraphEvents.CHANNEL_UPSERT,
|
|
||||||
{
|
|
||||||
id: channel.id,
|
|
||||||
resource: channel,
|
|
||||||
links: [],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.activityRepository.save(entity, context);
|
|
||||||
return new UpdateResult<ChannelActivity>("channel_activity", entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getChannelActivity(channel: Channel, context?: ExecutionContext): Promise<number> {
|
|
||||||
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<ChannelObject[]> {
|
|
||||||
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<ListResult<Channel | UserChannel>> {
|
|
||||||
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<ResourceEventsPayload>("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<ListResult<UserChannel>> {
|
|
||||||
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<string, ChannelActivity>;
|
|
||||||
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<string, ChannelActivity>(
|
|
||||||
activities.getEntities().map(activity => [activity.channel_id, activity]),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
activityPerChannel = new Map<string, ChannelActivity>();
|
|
||||||
}
|
|
||||||
|
|
||||||
channels.mapEntities(<UserChannel>(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(<UserDirectChannel>(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<DirectChannel> {
|
|
||||||
const instance = getDirectChannelInstance(directChannel);
|
|
||||||
await this.directChannelRepository.save(instance, context);
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDirectChannel(
|
|
||||||
directChannel: DirectChannel,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<DirectChannel> {
|
|
||||||
return this.directChannelRepository.findOne(directChannel, {}, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDirectChannelInCompany(
|
|
||||||
companyId: string,
|
|
||||||
users: string[],
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<DirectChannel> {
|
|
||||||
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<ListResult<Channel>> {
|
|
||||||
return await this.channelRepository.find(
|
|
||||||
{ company_id, workspace_id: "direct" },
|
|
||||||
{ pagination },
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDirectChannelsForUsersInCompany(
|
|
||||||
companyId: string,
|
|
||||||
userId: string,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<DirectChannel[]> {
|
|
||||||
const list = await this.directChannelRepository.find(
|
|
||||||
{
|
|
||||||
company_id: companyId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$like: [["users", userId]],
|
|
||||||
},
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
|
|
||||||
return list.getEntities();
|
|
||||||
}
|
|
||||||
|
|
||||||
async markAsRead(
|
|
||||||
pk: ChannelPrimaryKey,
|
|
||||||
user: Pick<User, "id">,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<boolean> {
|
|
||||||
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<boolean> {
|
|
||||||
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<Pick<ChannelPrimaryKey, "company_id" | "workspace_id">>,
|
|
||||||
): Promise<DefaultChannel[]> {
|
|
||||||
return this.defaultChannelService.getDefaultChannels(workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
async addUserToDefaultChannels(
|
|
||||||
user: User,
|
|
||||||
workspace: Required<Pick<ChannelPrimaryKey, "company_id" | "workspace_id">>,
|
|
||||||
): Promise<ChannelMember[]> {
|
|
||||||
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<Channel>,
|
|
||||||
): Promise<void> {
|
|
||||||
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<UsersIncludedChannel> {
|
|
||||||
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<Channel>,
|
|
||||||
mode: OperationType.CREATE | OperationType.UPDATE,
|
|
||||||
): Promise<void> {
|
|
||||||
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<ResourceEventsPayload>("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<Channel>): 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<ListResult<Channel>> {
|
|
||||||
const rep = gr.platformServices.search.getRepository<Channel>("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<Channel[]> {
|
|
||||||
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<ChannelObject, "id" | "company_id" | "workspace_id" | "stats">[],
|
|
||||||
) {
|
|
||||||
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 };
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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> = {},
|
|
||||||
): 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> = {}): 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;
|
|
||||||
};
|
|
||||||
@@ -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) }];
|
|
||||||
}
|
|
||||||
@@ -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<ChannelMember>;
|
|
||||||
channelMembersRepository: Repository<MemberOfChannel>;
|
|
||||||
channelMembersReadCursorRepository: Repository<ChannelMemberReadCursors>;
|
|
||||||
private channelCounter: CounterProvider<ChannelCounterEntity>;
|
|
||||||
private cache: NodeCache;
|
|
||||||
|
|
||||||
async init(): Promise<this> {
|
|
||||||
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<ChannelCounterEntity>(
|
|
||||||
ChannelCounterEntityType,
|
|
||||||
ChannelCounterEntity,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.channelCounter = await gr.platformServices.counter.getCounter<ChannelCounterEntity>(
|
|
||||||
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<ChannelMember>((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<SaveResult<ChannelMember>> {
|
|
||||||
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<Record<keyof ChannelMember, boolean>> = {
|
|
||||||
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<Partial<keyof ChannelMember>>;
|
|
||||||
|
|
||||||
if (!fields.length) {
|
|
||||||
return new SaveResult<ChannelMember>("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<ChannelMember>("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<ChannelMember>("channel_member", memberToSave),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw CrudException.badRequest(
|
|
||||||
`User ${member.user_id} is not allowed to join this channel`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SaveResult<ChannelMember>("channel_member", member, mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(pk: ChannelMemberPrimaryKey, context: ExecutionContext): Promise<ChannelMember> {
|
|
||||||
// FIXME: Who can fetch a single member?
|
|
||||||
return await this.userChannelsRepository.findOne(this.getPrimaryKey(pk), {}, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@RealtimeDeleted<ChannelMember>((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<DeleteResult<ChannelMember>> {
|
|
||||||
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<ChannelMember>("channel_member", pk as ChannelMember, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List given channel members
|
|
||||||
*/
|
|
||||||
async list(
|
|
||||||
pagination: Pagination,
|
|
||||||
options: ChannelListOptions,
|
|
||||||
context: ChannelExecutionContext,
|
|
||||||
): Promise<ListResult<ChannelMember>> {
|
|
||||||
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<ChannelMember>(
|
|
||||||
"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<string[]> {
|
|
||||||
let pagination = new Pagination(null);
|
|
||||||
const channels: string[] = [];
|
|
||||||
let result: ListResult<ChannelMember>;
|
|
||||||
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<ListResult<ChannelMember>> {
|
|
||||||
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<ChannelMember>(
|
|
||||||
"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<boolean> {
|
|
||||||
const list = this.list(new Pagination(undefined, "2"), {}, { channel, user });
|
|
||||||
|
|
||||||
return (await list).getEntities().length > 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addUsersToChannel(
|
|
||||||
users: Pick<User, "id">[] = [],
|
|
||||||
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<User, "id">,
|
|
||||||
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<ChannelMember>,
|
|
||||||
): 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<ChannelMember>,
|
|
||||||
): void {
|
|
||||||
logger.debug("Member created %o", member);
|
|
||||||
|
|
||||||
gr.platformServices.messageQueue.publish<ResourceEventsPayload>("channel:member:created", {
|
|
||||||
data: {
|
|
||||||
channel,
|
|
||||||
user,
|
|
||||||
member,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
localEventBus.publish<ResourceEventsPayload>("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<ResourceEventsPayload>("channel:member:deleted", {
|
|
||||||
data: {
|
|
||||||
channel,
|
|
||||||
user,
|
|
||||||
member,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
localEventBus.publish<ResourceEventsPayload>("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<Pick<Channel, "company_id" | "workspace_id" | "id">>,
|
|
||||||
cacheTtlSec?: number,
|
|
||||||
context?: ExecutionContext,
|
|
||||||
): Promise<ChannelMember> {
|
|
||||||
if (!user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cacheTtlSec) {
|
|
||||||
const pk = JSON.stringify({ user, channel });
|
|
||||||
if (this.cache.has(pk)) return this.cache.get<ChannelMember>(pk);
|
|
||||||
const entity = await this.getChannelMember(user, channel, undefined, context);
|
|
||||||
this.cache.set<ChannelMember>(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<number> {
|
|
||||||
return this.channelCounter.get(pk);
|
|
||||||
}
|
|
||||||
|
|
||||||
async isCompanyMember(companyId: string, userId: string): Promise<boolean> {
|
|
||||||
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<ListResult<ChannelMemberReadCursors>>} - The channel members read cursors.
|
|
||||||
*/
|
|
||||||
async getChannelMembersReadSections(
|
|
||||||
context: ChannelExecutionContext,
|
|
||||||
): Promise<ListResult<ChannelMemberReadCursors>> {
|
|
||||||
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<ChannelMemberReadCursors>(
|
|
||||||
"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<ChannelMemberReadCursors>} - The channel member read cursors.
|
|
||||||
*/
|
|
||||||
async getChannelMemberReadSections(
|
|
||||||
member: string,
|
|
||||||
context: ChannelExecutionContext,
|
|
||||||
): Promise<ChannelMemberReadCursors> {
|
|
||||||
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<SaveResult<ChannelMemberReadCursors>>} - The result of the save operation.
|
|
||||||
*/
|
|
||||||
async setChannelMemberReadSections(
|
|
||||||
section: { start: string; end: string },
|
|
||||||
context: CompanyExecutionContext & { channel_id: string; workspace_id: string },
|
|
||||||
): Promise<SaveResult<ChannelMemberReadCursors>> {
|
|
||||||
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<ChannelMemberReadCursors>(
|
|
||||||
"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<ChannelMemberReadCursors>(
|
|
||||||
"channel_members_read_cursors",
|
|
||||||
existingReadSection,
|
|
||||||
OperationType.EXISTS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
existingReadSection.read_section = [updatedReadSection.start, updatedReadSection.end];
|
|
||||||
await this.channelMembersReadCursorRepository.save(existingReadSection, context);
|
|
||||||
|
|
||||||
return new SaveResult<ChannelMemberReadCursors>(
|
|
||||||
"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<string[]>} - the promise containing the user id list
|
|
||||||
*/
|
|
||||||
async getChannelMessageSeenByUsers(
|
|
||||||
id: string,
|
|
||||||
context: ChannelExecutionContext,
|
|
||||||
): Promise<string[]> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<this> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ChannelActivityNotification, void>
|
|
||||||
{
|
|
||||||
private channelCounter: CounterProvider<ChannelCounterEntity>;
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
const channelCountersRepository = await gr.database.getRepository<ChannelCounterEntity>(
|
|
||||||
ChannelCounterEntityType,
|
|
||||||
ChannelCounterEntity,
|
|
||||||
);
|
|
||||||
this.channelCounter = await gr.platformServices.counter.getCounter<ChannelCounterEntity>(
|
|
||||||
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<void> {
|
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-58
@@ -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<NewUserInWorkspaceNotification, void>
|
|
||||||
{
|
|
||||||
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<void> {
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-59
@@ -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<NewUserInWorkspaceNotification, void>
|
|
||||||
{
|
|
||||||
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<void> {
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ChannelActivityNotification, void>
|
|
||||||
{
|
|
||||||
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<void> {
|
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ChannelTab>;
|
|
||||||
|
|
||||||
async init(): Promise<this> {
|
|
||||||
this.repository = await gr.database.getRepository("channel_tabs", ChannelTab);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@RealtimeSaved<ChannelTab>((tab, context) => [
|
|
||||||
{
|
|
||||||
room: ResourcePath.get(getTabsRealtimeRoom((context as ChannelExecutionContext).channel)),
|
|
||||||
path: getTabsRealtimeResourcePath(tab, (context as ChannelExecutionContext).channel),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
async save<SaveOptions>(
|
|
||||||
tab: ChannelTab,
|
|
||||||
options: SaveOptions,
|
|
||||||
context: ChannelExecutionContext,
|
|
||||||
): Promise<SaveResult<ChannelTab>> {
|
|
||||||
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<ChannelTab> {
|
|
||||||
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<ChannelTab>((tab, context) => [
|
|
||||||
{
|
|
||||||
room: ResourcePath.get(getTabsRealtimeRoom((context as ChannelExecutionContext).channel)),
|
|
||||||
path: getTabsRealtimeResourcePath(tab, (context as ChannelExecutionContext).channel),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
async delete(
|
|
||||||
pk: ChannelTabPrimaryKey,
|
|
||||||
context?: ExecutionContext,
|
|
||||||
): Promise<DeleteResult<ChannelTab>> {
|
|
||||||
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<ListOptions>(
|
|
||||||
pagination: Pagination,
|
|
||||||
options: ListOptions,
|
|
||||||
context: ChannelExecutionContext,
|
|
||||||
): Promise<ListResult<ChannelTab>> {
|
|
||||||
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<ChannelTab>,
|
|
||||||
): void {
|
|
||||||
console.log("Tab updated", tab);
|
|
||||||
}
|
|
||||||
|
|
||||||
onCreated(
|
|
||||||
channel: Channel,
|
|
||||||
tab: ChannelTab,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
createResult: SaveResult<ChannelTab>,
|
|
||||||
): 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}`;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { ChannelType } from "./types";
|
|
||||||
|
|
||||||
export function isDirectChannel(channel: { workspace_id: string }): boolean {
|
|
||||||
return channel?.workspace_id === ChannelType.DIRECT;
|
|
||||||
}
|
|
||||||
@@ -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<ChannelObject>,
|
|
||||||
ResourceCreateResponse<ChannelObject>,
|
|
||||||
ResourceListResponse<ChannelObject>,
|
|
||||||
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<ResourceGetResponse<ChannelObject>> {
|
|
||||||
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<ResourceListResponse<Channel>> {
|
|
||||||
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<Channel> {
|
|
||||||
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<Channel>) => {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<ResourceCreateResponse<ChannelObject>> {
|
|
||||||
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<ResourceUpdateResponse<ChannelObject>> {
|
|
||||||
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<ResourceListResponse<ChannelObject>> {
|
|
||||||
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<ResourceDeleteResponse> {
|
|
||||||
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<boolean> {
|
|
||||||
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<ResourceListResponse<ChannelPendingEmails>> {
|
|
||||||
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<ResourceCreateResponse<ChannelPendingEmails>> {
|
|
||||||
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<ResourceDeleteResponse> {
|
|
||||||
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<BaseChannelsParameters, "company_id">;
|
|
||||||
}>,
|
|
||||||
): Promise<ResourceListResponse<UsersIncludedChannel>> {
|
|
||||||
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",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from "./channel";
|
|
||||||
export * from "./member";
|
|
||||||
export * from "./tab";
|
|
||||||
@@ -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<ChannelMember>,
|
|
||||||
ResourceCreateResponse<ChannelMember>,
|
|
||||||
ResourceListResponse<ChannelMember>,
|
|
||||||
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<ResourceGetResponse<ChannelMember>> {
|
|
||||||
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<ResourceCreateResponse<ChannelMember>> {
|
|
||||||
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<ResourceUpdateResponse<ChannelMember>> {
|
|
||||||
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<ResourceDeleteResponse> {
|
|
||||||
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<ResourceListResponse<ChannelMemberWithUser>> {
|
|
||||||
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<Response> {
|
|
||||||
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<ResourceListResponse<ChannelMemberReadCursors>>} - the response
|
|
||||||
*/
|
|
||||||
async getAllChannelMembersReadSections(
|
|
||||||
request: FastifyRequest<{ Params: ChannelParameters }>,
|
|
||||||
reply: FastifyReply,
|
|
||||||
): Promise<ResourceListResponse<ChannelMemberReadCursors>> {
|
|
||||||
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<ResourceGetResponse<ChannelMemberReadCursors>>} - the read section for the member
|
|
||||||
*/
|
|
||||||
async getChannelMemberReadSections(
|
|
||||||
request: FastifyRequest<{ Params: ChannelMemberParameters }>,
|
|
||||||
reply: FastifyReply,
|
|
||||||
): Promise<ResourceGetResponse<ChannelMemberReadCursors>> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -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<ChannelTab>,
|
|
||||||
ResourceCreateResponse<ChannelTab>,
|
|
||||||
ResourceListResponse<ChannelTab>,
|
|
||||||
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<ResourceGetResponse<ChannelTab>> {
|
|
||||||
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<ResourceCreateResponse<ChannelTab>> {
|
|
||||||
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<ResourceEventsPayload>("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<ResourceUpdateResponse<ChannelTab>> {
|
|
||||||
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<ResourceDeleteResponse> {
|
|
||||||
try {
|
|
||||||
const context = getExecutionContext(request);
|
|
||||||
const deleteResult = await gr.services.tab.delete(this.getPrimaryKey(request));
|
|
||||||
|
|
||||||
if (deleteResult.deleted) {
|
|
||||||
localEventBus.publish<ResourceEventsPayload>("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<ResourceListResponse<ChannelTab>> {
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { validate as validateUuid } from "uuid";
|
|
||||||
import { ChannelVisibility } from "../types";
|
|
||||||
|
|
||||||
export function checkCompanyAndWorkspaceForUser(
|
|
||||||
companyId: string,
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
return Promise.resolve(
|
|
||||||
validateUuid(companyId) &&
|
|
||||||
(validateUuid(workspaceId) || workspaceId === ChannelVisibility.DIRECT),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -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<Channel, "name">;
|
|
||||||
|
|
||||||
export type ChannelUpdateResource = Pick<Channel, "name">;
|
|
||||||
|
|
||||||
// channel members
|
|
||||||
|
|
||||||
export class CreateChannelMemberBody {
|
|
||||||
resource: Pick<ChannelMember, "user_id">;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UpdateChannelMemberBody {
|
|
||||||
resource: Pick<ChannelMember, "favorite" | "notification_level">;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,6 @@ import { PushServiceAPI } from "../core/platform/services/push/api";
|
|||||||
import { CronAPI } from "../core/platform/services/cron/api";
|
import { CronAPI } from "../core/platform/services/cron/api";
|
||||||
import WebSocketAPI from "../core/platform/services/websocket/provider";
|
import WebSocketAPI from "../core/platform/services/websocket/provider";
|
||||||
import TrackerAPI from "../core/platform/services/tracker/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 EmailPusherAPI from "../core/platform/services/email-pusher/provider";
|
||||||
|
|
||||||
import { logger } from "../core/platform/framework";
|
import { logger } from "../core/platform/framework";
|
||||||
@@ -22,26 +21,14 @@ import assert from "assert";
|
|||||||
import { CompanyServiceImpl } from "./user/services/companies";
|
import { CompanyServiceImpl } from "./user/services/companies";
|
||||||
import { WorkspaceServiceImpl } from "./workspaces/services/workspace";
|
import { WorkspaceServiceImpl } from "./workspaces/services/workspace";
|
||||||
import { UserExternalLinksServiceImpl } from "./user/services/external_links";
|
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 { UserServiceImpl } from "./user/services/users/service";
|
||||||
import { CompanyApplicationServiceImpl } from "./applications/services/company-applications";
|
import { CompanyApplicationServiceImpl } from "./applications/services/company-applications";
|
||||||
import { ApplicationServiceImpl } from "./applications/services/applications";
|
import { ApplicationServiceImpl } from "./applications/services/applications";
|
||||||
import { FileServiceImpl } from "./files/services";
|
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 { ConsoleServiceImpl } from "./console/service";
|
||||||
import { StatisticsServiceImpl } from "./statistics/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 { ApplicationHooksService } from "./applications/services/hooks";
|
||||||
import OnlineServiceImpl from "./online/service";
|
import OnlineServiceImpl from "./online/service";
|
||||||
import { ChannelsMessageQueueListener } from "./channels/services/pubsub";
|
|
||||||
import { UserNotificationDigestService } from "./notifications/services/digest";
|
|
||||||
import { DocumentsService } from "./documents/services";
|
import { DocumentsService } from "./documents/services";
|
||||||
import { DocumentsEngine } from "./documents/services/engine";
|
import { DocumentsEngine } from "./documents/services/engine";
|
||||||
import { TagsService } from "./tags/services/tags";
|
import { TagsService } from "./tags/services/tags";
|
||||||
@@ -58,7 +45,6 @@ type PlatformServices = {
|
|||||||
tracker: TrackerAPI;
|
tracker: TrackerAPI;
|
||||||
webserver: WebServerAPI;
|
webserver: WebServerAPI;
|
||||||
websocket: WebSocketAPI;
|
websocket: WebSocketAPI;
|
||||||
knowledgeGraph: KnowledgeGraphService;
|
|
||||||
emailPusher: EmailPusherAPI;
|
emailPusher: EmailPusherAPI;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,28 +55,12 @@ type TdriveServices = {
|
|||||||
console: ConsoleServiceImpl;
|
console: ConsoleServiceImpl;
|
||||||
statistics: StatisticsServiceImpl;
|
statistics: StatisticsServiceImpl;
|
||||||
externalUser: UserExternalLinksServiceImpl;
|
externalUser: UserExternalLinksServiceImpl;
|
||||||
notifications: {
|
|
||||||
badges: UserNotificationBadgeService;
|
|
||||||
channelPreferences: ChannelMemberPreferencesServiceImpl;
|
|
||||||
channelThreads: ChannelThreadUsersServiceImpl;
|
|
||||||
engine: NotificationEngine;
|
|
||||||
preferences: NotificationPreferencesService;
|
|
||||||
mobilePush: MobilePushService;
|
|
||||||
digest: UserNotificationDigestService;
|
|
||||||
};
|
|
||||||
applications: {
|
applications: {
|
||||||
marketplaceApps: ApplicationServiceImpl;
|
marketplaceApps: ApplicationServiceImpl;
|
||||||
companyApps: CompanyApplicationServiceImpl;
|
companyApps: CompanyApplicationServiceImpl;
|
||||||
hooks: ApplicationHooksService;
|
hooks: ApplicationHooksService;
|
||||||
};
|
};
|
||||||
files: FileServiceImpl;
|
files: FileServiceImpl;
|
||||||
channels: {
|
|
||||||
channels: ChannelServiceImpl;
|
|
||||||
members: MemberServiceImpl;
|
|
||||||
pubsub: ChannelsMessageQueueListener;
|
|
||||||
};
|
|
||||||
channelPendingEmail: ChannelPendingEmailServiceImpl;
|
|
||||||
tab: TabServiceImpl;
|
|
||||||
online: OnlineServiceImpl;
|
online: OnlineServiceImpl;
|
||||||
documents: {
|
documents: {
|
||||||
documents: DocumentsService;
|
documents: DocumentsService;
|
||||||
@@ -126,7 +96,6 @@ class GlobalResolver {
|
|||||||
tracker: platform.getProvider<TrackerAPI>("tracker"),
|
tracker: platform.getProvider<TrackerAPI>("tracker"),
|
||||||
webserver: platform.getProvider<WebServerAPI>("webserver"),
|
webserver: platform.getProvider<WebServerAPI>("webserver"),
|
||||||
websocket: platform.getProvider<WebSocketAPI>("websocket"),
|
websocket: platform.getProvider<WebSocketAPI>("websocket"),
|
||||||
knowledgeGraph: await new KnowledgeGraphService().init(),
|
|
||||||
emailPusher: platform.getProvider<EmailPusherAPI>("email-pusher"),
|
emailPusher: platform.getProvider<EmailPusherAPI>("email-pusher"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,28 +113,12 @@ class GlobalResolver {
|
|||||||
console: await new ConsoleServiceImpl().init(),
|
console: await new ConsoleServiceImpl().init(),
|
||||||
statistics: await new StatisticsServiceImpl().init(),
|
statistics: await new StatisticsServiceImpl().init(),
|
||||||
externalUser: await new UserExternalLinksServiceImpl().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: {
|
applications: {
|
||||||
marketplaceApps: await new ApplicationServiceImpl().init(),
|
marketplaceApps: await new ApplicationServiceImpl().init(),
|
||||||
companyApps: await new CompanyApplicationServiceImpl().init(),
|
companyApps: await new CompanyApplicationServiceImpl().init(),
|
||||||
hooks: await new ApplicationHooksService().init(),
|
hooks: await new ApplicationHooksService().init(),
|
||||||
},
|
},
|
||||||
files: await new FileServiceImpl().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(),
|
online: await new OnlineServiceImpl().init(),
|
||||||
documents: {
|
documents: {
|
||||||
documents: await new DocumentsService().init(),
|
documents: await new DocumentsService().init(),
|
||||||
|
|||||||
@@ -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<undefined> {
|
|
||||||
version = "1";
|
|
||||||
name = "knowledge-graph";
|
|
||||||
|
|
||||||
api(): undefined {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async doInit(): Promise<this> {
|
|
||||||
const fastify = this.context.getProvider<WebServerAPI>("webserver").getServer();
|
|
||||||
|
|
||||||
fastify.register((instance, _opts, next) => {
|
|
||||||
web(instance, { prefix: this.prefix });
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
@@ -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<any> => {
|
|
||||||
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;
|
|
||||||
-50
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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";
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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<undefined> {
|
|
||||||
version = "1";
|
|
||||||
name = "notifications";
|
|
||||||
|
|
||||||
public async doInit(): Promise<this> {
|
|
||||||
const fastify = this.context.getProvider<WebServerAPI>("webserver").getServer();
|
|
||||||
fastify.register((instance, _opts, next) => {
|
|
||||||
web(instance, { prefix: this.prefix });
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
// TODO: remove
|
|
||||||
api(): undefined {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export * from "./mobile-push";
|
|
||||||
|
|
||||||
export interface Notifier {
|
|
||||||
notify<Message>(user: string, message: Message): Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -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<Message>(user: string, message: Message): Promise<void> {
|
|
||||||
logger.info(`MobilePushNotifier - Push to mobile ${user}`);
|
|
||||||
logger.debug(`MobilePushNotifier - Push to mobile ${user}, ${JSON.stringify(message)}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.messageQueue.publish<Message>(TOPIC, {
|
|
||||||
data: message,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn({ err }, `MobilePushNotifier - Error while sending notification to user ${user}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<UserNotificationBadge>;
|
|
||||||
|
|
||||||
async init(context: TdriveContext): Promise<this> {
|
|
||||||
this.repository = await gr.database.getRepository<UserNotificationBadge>(
|
|
||||||
UserNotificationBadgeType,
|
|
||||||
UserNotificationBadge,
|
|
||||||
);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(
|
|
||||||
pk: UserNotificationBadgePrimaryKey,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<UserNotificationBadge> {
|
|
||||||
return await this.repository.findOne(pk, {}, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@RealtimeSaved<UserNotificationBadge>((badge, context) => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
room: ResourcePath.get(getNotificationRoomName(badge.user_id)),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
})
|
|
||||||
async save<SaveOptions>(
|
|
||||||
badge: UserNotificationBadge,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<SaveResult<UserNotificationBadge>> {
|
|
||||||
//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<UserNotificationBadge>((badge, context) => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
room: ResourcePath.get(getNotificationRoomName(badge.user_id)),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
})
|
|
||||||
async delete(
|
|
||||||
pk: UserNotificationBadgePrimaryKey,
|
|
||||||
context?: NotificationExecutionContext,
|
|
||||||
): Promise<DeleteResult<UserNotificationBadge>> {
|
|
||||||
//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<ListResult<UserNotificationBadge>> {
|
|
||||||
throw new Error("Not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
async listForUserPerCompanies(
|
|
||||||
user_id: string,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<ListResult<UserNotificationBadge>> {
|
|
||||||
//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<UserNotificationBadgePrimaryKey, "workspace_id" | "channel_id" | "thread_id">,
|
|
||||||
context?: ExecutionContext,
|
|
||||||
): Promise<ListResult<UserNotificationBadge>> {
|
|
||||||
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<UserNotificationBadge>,
|
|
||||||
context?: ExecutionContext,
|
|
||||||
): Promise<ListResult<UserNotificationBadge>> {
|
|
||||||
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<number> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ChannelMemberNotificationPreference>;
|
|
||||||
|
|
||||||
async init(): Promise<this> {
|
|
||||||
this.repository = await gr.database.getRepository<ChannelMemberNotificationPreference>(
|
|
||||||
TYPE,
|
|
||||||
ChannelMemberNotificationPreference,
|
|
||||||
);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(
|
|
||||||
entity: ChannelMemberNotificationPreference,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<SaveResult<ChannelMemberNotificationPreference>> {
|
|
||||||
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<ChannelMemberNotificationPreference> {
|
|
||||||
return await this.repository.findOne(pk, {}, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(
|
|
||||||
pk: ChannelMemberNotificationPreferencePrimaryKey,
|
|
||||||
context?: ExecutionContext,
|
|
||||||
): Promise<DeleteResult<ChannelMemberNotificationPreference>> {
|
|
||||||
await this.repository.remove(pk as ChannelMemberNotificationPreference, context);
|
|
||||||
|
|
||||||
return new DeleteResult(TYPE, pk as ChannelMemberNotificationPreference, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
list(): Promise<ListResult<ChannelMemberNotificationPreference>> {
|
|
||||||
throw new Error("Not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
async getChannelPreferencesForUsers(
|
|
||||||
channelAndCompany: Pick<
|
|
||||||
ChannelMemberNotificationPreferencePrimaryKey,
|
|
||||||
"channel_id" | "company_id"
|
|
||||||
>,
|
|
||||||
users?: string[] | null,
|
|
||||||
lastRead?: {
|
|
||||||
lessThan: number;
|
|
||||||
},
|
|
||||||
context?: ExecutionContext,
|
|
||||||
): Promise<ListResult<ChannelMemberNotificationPreference>> {
|
|
||||||
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<ListResult<ChannelMemberNotificationPreference>> {
|
|
||||||
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<ChannelMemberNotificationPreference, "channel_id" | "company_id">,
|
|
||||||
user_id: string,
|
|
||||||
lastRead: number,
|
|
||||||
context?: ExecutionContext,
|
|
||||||
): Promise<ChannelMemberNotificationPreference> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ChannelThreadUsers>;
|
|
||||||
|
|
||||||
async init(): Promise<this> {
|
|
||||||
this.repository = await gr.database.getRepository<ChannelThreadUsers>(
|
|
||||||
ChannelThreadUsersType,
|
|
||||||
ChannelThreadUsers,
|
|
||||||
);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async bulkSave(
|
|
||||||
entities: ChannelThreadUsers[],
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<SaveResult<ChannelThreadUsers[]>> {
|
|
||||||
await this.repository.saveAll(entities, context);
|
|
||||||
|
|
||||||
return new SaveResult(ChannelThreadUsersType, entities, OperationType.CREATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(
|
|
||||||
pk: ChannelThreadUsersPrimaryKey,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<ChannelThreadUsers> {
|
|
||||||
return await this.repository.findOne(pk, {}, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(
|
|
||||||
pk: ChannelThreadUsersPrimaryKey,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<DeleteResult<ChannelThreadUsers>> {
|
|
||||||
await this.repository.remove(pk as ChannelThreadUsers, context);
|
|
||||||
|
|
||||||
return new DeleteResult(ChannelThreadUsersType, pk as ChannelThreadUsers, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
list(): Promise<ListResult<ChannelThreadUsers>> {
|
|
||||||
throw new Error("Not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUsersInThread(
|
|
||||||
pk: ChannelThreadUsersPrimaryKey,
|
|
||||||
context: ExecutionContext,
|
|
||||||
): Promise<ListResult<ChannelThreadUsers>> {
|
|
||||||
if (!pk.company_id || !pk.channel_id || !pk.thread_id) {
|
|
||||||
throw new Error("Missing required fields");
|
|
||||||
}
|
|
||||||
return this.repository.find(pk, {}, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<UserNotificationDigest>;
|
|
||||||
|
|
||||||
async init(): Promise<this> {
|
|
||||||
this.repository = await gr.database.getRepository<UserNotificationDigest>(
|
|
||||||
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<UserNotificationDigest> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<this> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-79
@@ -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<JoinChannelMessage, void>
|
|
||||||
{
|
|
||||||
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<void> {
|
|
||||||
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`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-101
@@ -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<LeaveChannelMessage, void>
|
|
||||||
{
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-55
@@ -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<UpdateChannelMessage, void>
|
|
||||||
{
|
|
||||||
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<void> {
|
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-70
@@ -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<ChannelReadMessage, void>
|
|
||||||
{
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-68
@@ -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<ChannelUnreadMessage, void>
|
|
||||||
{
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-48
@@ -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<PushNotificationMessage, PushNotificationMessageResult>
|
|
||||||
{
|
|
||||||
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<PushNotificationMessageResult> {
|
|
||||||
logger.info(
|
|
||||||
`${this.name} - Processing mobile push notification for channel ${message.channel_id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await gr.services.notifications.mobilePush.push(message);
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-213
@@ -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<MentionNotification, MentionNotificationResult>
|
|
||||||
{
|
|
||||||
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<MentionNotificationResult> {
|
|
||||||
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<ChannelMemberNotificationPreference, "channel_id" | "company_id">,
|
|
||||||
users: string[] = [],
|
|
||||||
timestamp: number,
|
|
||||||
context?: ExecutionContext,
|
|
||||||
): Promise<string[]> {
|
|
||||||
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<Array<UserNotificationBadge>> {
|
|
||||||
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<UserNotificationBadge> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
-127
@@ -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<ReactionNotification, void>
|
|
||||||
{
|
|
||||||
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<void> {
|
|
||||||
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}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<User>;
|
|
||||||
|
|
||||||
async init(): Promise<this> {
|
|
||||||
this.userRepository = await gr.database.getRepository<User>(UserType, User);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Fixme: add a bulk system to group requests to fcm
|
|
||||||
async push(message: PushNotificationMessage, context?: ExecutionContext): Promise<void> {
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<UserNotificationPreferences>;
|
|
||||||
|
|
||||||
async init(): Promise<this> {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async list(): Promise<ListResult<UserNotificationPreferences>> {
|
|
||||||
throw new Error("Not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(
|
|
||||||
pk: UserNotificationPreferencesPrimaryKey,
|
|
||||||
user?: User,
|
|
||||||
): Promise<UserNotificationPreferences> {
|
|
||||||
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<UserNotificationPreferences> {
|
|
||||||
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<DeleteResult<UserNotificationPreferences>> {
|
|
||||||
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<ListResult<UserNotificationPreferences>> {
|
|
||||||
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<SaveResult<UserNotificationPreferences>> {
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}`;
|
|
||||||
}
|
|
||||||
@@ -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<InputMessage, OutputMessage> = 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;
|
|
||||||
};
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./notification";
|
|
||||||
export * from "./preferences";
|
|
||||||
@@ -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<UserNotificationBadge>,
|
|
||||||
ResourceCreateResponse<UserNotificationBadge>,
|
|
||||||
ResourceListResponse<UserNotificationBadge>,
|
|
||||||
ResourceDeleteResponse
|
|
||||||
>
|
|
||||||
{
|
|
||||||
async list(
|
|
||||||
request: FastifyRequest<{
|
|
||||||
Querystring: NotificationListQueryParameters;
|
|
||||||
}>,
|
|
||||||
): Promise<ResourceListResponse<UserNotificationBadge>> {
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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<ResourceListResponse<UserNotificationPreferences>> {
|
|
||||||
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<ResourceCreateResponse<UserNotificationPreferences>> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
@@ -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"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -34,10 +34,6 @@ import { logger, RealtimeSaved } from "../../../core/platform/framework";
|
|||||||
import { getCompanyRoom, getUserRoom } from "../realtime";
|
import { getCompanyRoom, getUserRoom } from "../realtime";
|
||||||
import gr from "../../global-resolver";
|
import gr from "../../global-resolver";
|
||||||
import { localEventBus } from "../../../core/platform/framework/event-bus";
|
import { localEventBus } from "../../../core/platform/framework/event-bus";
|
||||||
import {
|
|
||||||
KnowledgeGraphEvents,
|
|
||||||
KnowledgeGraphGenericEventPayload,
|
|
||||||
} from "../../../core/platform/services/knowledge-graph/types";
|
|
||||||
|
|
||||||
export class CompanyServiceImpl {
|
export class CompanyServiceImpl {
|
||||||
version: "1";
|
version: "1";
|
||||||
@@ -100,15 +96,6 @@ export class CompanyServiceImpl {
|
|||||||
await this.externalCompanyRepository.save(extCompany, context);
|
await this.externalCompanyRepository.save(extCompany, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
localEventBus.publish<KnowledgeGraphGenericEventPayload<Company>>(
|
|
||||||
KnowledgeGraphEvents.COMPANY_UPSERT,
|
|
||||||
{
|
|
||||||
id: company.id,
|
|
||||||
resource: company,
|
|
||||||
links: [],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return new SaveResult<Company>("company", company, OperationType.UPDATE);
|
return new SaveResult<Company>("company", company, OperationType.UPDATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,10 +32,6 @@ import { RealtimeSaved } from "../../../../core/platform/framework";
|
|||||||
import { getPublicUserRoom, getUserRoom } from "../../realtime";
|
import { getPublicUserRoom, getUserRoom } from "../../realtime";
|
||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import gr from "../../../global-resolver";
|
import gr from "../../../global-resolver";
|
||||||
import {
|
|
||||||
KnowledgeGraphEvents,
|
|
||||||
KnowledgeGraphGenericEventPayload,
|
|
||||||
} from "../../../../core/platform/services/knowledge-graph/types";
|
|
||||||
import { formatUser } from "../../../../utils/users";
|
import { formatUser } from "../../../../utils/users";
|
||||||
|
|
||||||
export class UserServiceImpl {
|
export class UserServiceImpl {
|
||||||
@@ -121,27 +117,11 @@ export class UserServiceImpl {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
async save<SaveOptions>(user: User, context?: ExecutionContext): Promise<SaveResult<User>> {
|
async save(user: User, context?: ExecutionContext): Promise<SaveResult<User>> {
|
||||||
this.assignDefaults(user);
|
this.assignDefaults(user);
|
||||||
await this.repository.save(user, context);
|
await this.repository.save(user, context);
|
||||||
await this.updateExtRepository(user);
|
await this.updateExtRepository(user);
|
||||||
|
|
||||||
localEventBus.publish<KnowledgeGraphGenericEventPayload<User>>(
|
|
||||||
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);
|
await this.publishPublicUserRealtime(user.id);
|
||||||
|
|
||||||
return new SaveResult("user", user, OperationType.UPDATE);
|
return new SaveResult("user", user, OperationType.UPDATE);
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import { getCompanyRooms, getUserRooms } from "../realtime";
|
|||||||
import { formatCompany, getCompanyStats } from "../utils";
|
import { formatCompany, getCompanyStats } from "../utils";
|
||||||
import { formatUser } from "../../../utils/users";
|
import { formatUser } from "../../../utils/users";
|
||||||
import gr from "../../global-resolver";
|
import gr from "../../global-resolver";
|
||||||
import { UserChannel, UsersIncludedChannel } from "../../channels/entities";
|
|
||||||
|
|
||||||
export class UsersCrudController
|
export class UsersCrudController
|
||||||
implements
|
implements
|
||||||
@@ -282,39 +281,11 @@ export class UsersCrudController
|
|||||||
}
|
}
|
||||||
|
|
||||||
async recent(
|
async recent(
|
||||||
request: FastifyRequest<{ Params: CompanyParameters; Querystring: { limit: 100 } }>,
|
_request: FastifyRequest<{ Params: CompanyParameters; Querystring: { limit: 100 } }>,
|
||||||
_reply: FastifyReply,
|
_reply: FastifyReply,
|
||||||
): Promise<ResourceListResponse<UserObject>> {
|
): Promise<ResourceListResponse<UserObject>> {
|
||||||
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<UsersIncludedChannel>,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const users: UserObject[] = [];
|
|
||||||
for (const channel of userIncludedChannels) {
|
|
||||||
for (const user of channel.users) {
|
|
||||||
if (user.id != request.currentUser.id) users.push(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
resources: [...uniq(users).slice(0, request.query.limit)],
|
resources: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,10 +60,6 @@ import { expandUUID4, reduceUUID4 } from "../../../utils/uuid-reducer";
|
|||||||
import gr from "../../global-resolver";
|
import gr from "../../global-resolver";
|
||||||
import { logger } from "@sentry/utils";
|
import { logger } from "@sentry/utils";
|
||||||
import { localEventBus } from "../../../core/platform/framework/event-bus";
|
import { localEventBus } from "../../../core/platform/framework/event-bus";
|
||||||
import {
|
|
||||||
KnowledgeGraphEvents,
|
|
||||||
KnowledgeGraphGenericEventPayload,
|
|
||||||
} from "../../../core/platform/services/knowledge-graph/types";
|
|
||||||
import WorkspaceInviteDomain, {
|
import WorkspaceInviteDomain, {
|
||||||
TYPE as WorkspaceInviteDomainType,
|
TYPE as WorkspaceInviteDomainType,
|
||||||
getInstance as getWorkspaceInviteDomainInstance,
|
getInstance as getWorkspaceInviteDomainInstance,
|
||||||
@@ -239,15 +235,6 @@ export class WorkspaceServiceImpl implements TdriveServiceProvider, Initializabl
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
localEventBus.publish<KnowledgeGraphGenericEventPayload<Workspace>>(
|
|
||||||
KnowledgeGraphEvents.WORKSPACE_UPSERT,
|
|
||||||
{
|
|
||||||
id: workspace.id,
|
|
||||||
resource: workspace,
|
|
||||||
links: [{ relation: "parent", type: "company", id: workspace.company_id }],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return new SaveResult<Workspace>(
|
return new SaveResult<Workspace>(
|
||||||
TYPE,
|
TYPE,
|
||||||
workspace,
|
workspace,
|
||||||
|
|||||||
-17
@@ -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.company.id = company.id;
|
||||||
resource.workspace.id = workspace.id;
|
resource.workspace.id = workspace.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import WorkspacePendingUser from "../../entities/workspace_pending_users";
|
|||||||
import { ConsoleCompany, CreateConsoleUser } from "../../../console/types";
|
import { ConsoleCompany, CreateConsoleUser } from "../../../console/types";
|
||||||
import { hasCompanyAdminLevel } from "../../../../utils/company";
|
import { hasCompanyAdminLevel } from "../../../../utils/company";
|
||||||
import gr from "../../../global-resolver";
|
import gr from "../../../global-resolver";
|
||||||
import { getChannelPendingEmailsInstance } from "../../../channels/entities";
|
|
||||||
|
|
||||||
export class WorkspaceUsersCrudController
|
export class WorkspaceUsersCrudController
|
||||||
implements
|
implements
|
||||||
@@ -413,16 +412,6 @@ export class WorkspaceUsersCrudController
|
|||||||
invitation.role,
|
invitation.role,
|
||||||
invitation.company_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" });
|
responses.push({ email: invitation.email, status: "ok" });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -516,18 +505,6 @@ export class WorkspaceUsersCrudController
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
usersToProcessImmediately.map(user => {
|
usersToProcessImmediately.map(user => {
|
||||||
gr.services.console.processPendingUser(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,
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { WorkspacePrimaryKey } from "../services/workspaces/entities/workspace";
|
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;
|
export type uuid = string;
|
||||||
|
|
||||||
@@ -102,14 +95,9 @@ export interface ResourceWebsocket {
|
|||||||
|
|
||||||
export interface ResourceEventsPayload {
|
export interface ResourceEventsPayload {
|
||||||
user: User;
|
user: User;
|
||||||
channel?: ChannelEntity;
|
|
||||||
channelParameters?: ChannelParameters;
|
|
||||||
guest?: ChannelPendingEmails;
|
|
||||||
member?: ChannelMember;
|
|
||||||
actor?: User;
|
actor?: User;
|
||||||
resourcesBefore?: (User | ChannelEntity | ChannelTab | ChannelMember)[];
|
resourcesBefore?: User[];
|
||||||
resourcesAfter?: (User | ChannelEntity | ChannelTab | ChannelMember)[];
|
resourcesAfter?: User[];
|
||||||
tab?: ChannelTab;
|
|
||||||
company?: { id: string };
|
company?: { id: string };
|
||||||
workspace?: WorkspacePrimaryKey;
|
workspace?: WorkspacePrimaryKey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Tag> = 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<Tag> = 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<Tag> = 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<Tag> = 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<Tag> = 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<Tag> = 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<Tag> = 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+16
-31
@@ -5,79 +5,64 @@ import {
|
|||||||
buildComparison,
|
buildComparison,
|
||||||
buildIn,
|
buildIn,
|
||||||
} from "../../../../../../../../../src/core/platform/services/database/services/orm/connectors/cassandra/query-builder";
|
} 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 QueryBuilder module", () => {
|
||||||
describe("The buildSelectQuery function", () => {
|
describe("The buildSelectQuery function", () => {
|
||||||
it("should build a valid query from primary key parameters", () => {
|
it("should build a valid query from primary key parameters", () => {
|
||||||
const filters = {
|
const filters = {
|
||||||
company_id: "comp1",
|
company_id: "comp1",
|
||||||
channel_id: "chan1",
|
parent_id: "parent1",
|
||||||
};
|
};
|
||||||
const result = buildSelectQuery<ChannelMemberNotificationPreference>(
|
const result = buildSelectQuery<DriveFile>(DriveFile, filters, {}, { keyspace: "tdrive" });
|
||||||
ChannelMemberNotificationPreference,
|
|
||||||
filters,
|
|
||||||
{},
|
|
||||||
{ keyspace: "tdrive" },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
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", () => {
|
it("should build a valid query from primary key parameters and comparison", () => {
|
||||||
const filters = {
|
const filters = {
|
||||||
company_id: "comp1",
|
company_id: "comp1",
|
||||||
channel_id: "chan1",
|
parent_id: "parent1",
|
||||||
};
|
};
|
||||||
const result = buildSelectQuery<ChannelMemberNotificationPreference>(
|
const result = buildSelectQuery<DriveFile>(
|
||||||
ChannelMemberNotificationPreference,
|
DriveFile,
|
||||||
filters,
|
filters,
|
||||||
{
|
{
|
||||||
$lt: [["last_read", 1000]],
|
$lt: [["size", 1000]],
|
||||||
},
|
},
|
||||||
{ keyspace: "tdrive" },
|
{ keyspace: "tdrive" },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual(
|
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", () => {
|
it("should build IN query from array parameters", () => {
|
||||||
const filters = {
|
const filters = {
|
||||||
company_id: "comp1",
|
company_id: "comp1",
|
||||||
channel_id: "chan1",
|
parent_id: "parent1",
|
||||||
user_id: ["u1", "u2", "u3"],
|
creator: ["u1", "u2", "u3"],
|
||||||
};
|
};
|
||||||
const result = buildSelectQuery<ChannelMemberNotificationPreference>(
|
const result = buildSelectQuery<DriveFile>(DriveFile, filters, {}, { keyspace: "tdrive" });
|
||||||
ChannelMemberNotificationPreference,
|
|
||||||
filters,
|
|
||||||
{},
|
|
||||||
{ keyspace: "tdrive" },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
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", () => {
|
it("should not build IN query from array parameters when array is empty", () => {
|
||||||
const filters = {
|
const filters = {
|
||||||
company_id: "comp1",
|
company_id: "comp1",
|
||||||
channel_id: "chan1",
|
parent_id: "parent1",
|
||||||
user_id: [],
|
user_id: [],
|
||||||
};
|
};
|
||||||
const result = buildSelectQuery<ChannelMemberNotificationPreference>(
|
const result = buildSelectQuery<DriveFile>(DriveFile, filters, {}, { keyspace: "tdrive" });
|
||||||
ChannelMemberNotificationPreference,
|
|
||||||
filters,
|
|
||||||
{},
|
|
||||||
{ keyspace: "tdrive" },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
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';",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user