🧹 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"
|
||||
}
|
||||
},
|
||||
"phpnode": {
|
||||
"secret": "PHP_NODE_API_SECRET"
|
||||
},
|
||||
"websocket": {
|
||||
"auth": {
|
||||
"jwt": {
|
||||
@@ -44,15 +41,6 @@
|
||||
"secret": "AUTH_JWT_SECRET"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"push": {
|
||||
"type": "PUSH_DRIVER",
|
||||
"fcm": {
|
||||
"endpoint": "PUSH_FCM_ENDPOINT",
|
||||
"key": "PUSH_FCM_KEY"
|
||||
}
|
||||
}
|
||||
},
|
||||
"database": {
|
||||
"secret": "DB_SECRET",
|
||||
"type": "DB_DRIVER",
|
||||
|
||||
@@ -127,12 +127,6 @@
|
||||
"path": "/storage/"
|
||||
}
|
||||
},
|
||||
"knowledge-graph": {
|
||||
"endpoint": "http://host-gateway:8888",
|
||||
"callback_token": "secret",
|
||||
"use": false,
|
||||
"forwarded_companies": []
|
||||
},
|
||||
"email-pusher": {
|
||||
"endpoint": "https://api.smtp2go.com/v3/email/send",
|
||||
"api_key": "secret",
|
||||
@@ -153,12 +147,9 @@
|
||||
"search",
|
||||
"message-queue",
|
||||
"realtime",
|
||||
"phpnode",
|
||||
"tracker",
|
||||
"general",
|
||||
"user",
|
||||
"channels",
|
||||
"notifications",
|
||||
"files",
|
||||
"workspaces",
|
||||
"console",
|
||||
@@ -166,8 +157,6 @@
|
||||
"statistics",
|
||||
"cron",
|
||||
"online",
|
||||
"knowledge-graph",
|
||||
"knowledge-graph-web",
|
||||
"email-pusher",
|
||||
"documents",
|
||||
"applications",
|
||||
|
||||
@@ -19,8 +19,5 @@
|
||||
"amqp": {
|
||||
"urls": ["amqp://guest:guest@rabbitmq:5672"]
|
||||
}
|
||||
},
|
||||
"phpnode": {
|
||||
"php_endpoint": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ export interface authenticateDecorator {
|
||||
|
||||
declare module "fastify" {
|
||||
interface FastifyInstance {
|
||||
phpnodeAuthenticate(): void;
|
||||
authenticate(): void;
|
||||
authenticateOptional(): void;
|
||||
io: SocketIO.Server;
|
||||
|
||||
@@ -3,8 +3,6 @@ import tdrive from "../../../tdrive";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { Pagination } from "../../../core/platform/framework/api/crud-service";
|
||||
import WorkspaceUser from "../../../services/workspaces/entities/workspace_user";
|
||||
import { ChannelVisibility } from "../../../services/channels/types";
|
||||
import { Channel, ChannelMember } from "../../../services/channels/entities";
|
||||
import { formatCompany } from "../../../services/user/utils";
|
||||
import gr from "../../../services/global-resolver";
|
||||
import { formatUser } from "../../../utils/users";
|
||||
@@ -105,91 +103,6 @@ const command: yargs.CommandModule<unknown, CLIArgs> = {
|
||||
}
|
||||
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();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ import { DatabaseServiceAPI } from "../../../core/platform/services/database/api
|
||||
import { Pagination } from "../../../core/platform/framework/api/crud-service";
|
||||
|
||||
import User, { TYPE as UserTYPE } from "../../../services/user/entities/user";
|
||||
import { Channel } from "../../../services/channels/entities";
|
||||
import Repository from "../../../core/platform/services/database/services/orm/repository/repository";
|
||||
import { SearchServiceAPI } from "../../../core/platform/services/search/api";
|
||||
import CompanyUser, { TYPE as CompanyUserTYPE } from "../../../services/user/entities/company_user";
|
||||
@@ -29,7 +28,6 @@ class SearchIndexAll {
|
||||
public async run(options: Options = {}): Promise<void> {
|
||||
const repositories: Map<string, Repository<any>> = new Map();
|
||||
repositories.set("users", await this.database.getRepository(UserTYPE, User));
|
||||
repositories.set("channels", await this.database.getRepository("channels", Channel));
|
||||
|
||||
const repository = repositories.get(options.repository);
|
||||
if (!repository) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Channel } from "../../../../services/channels/entities/channel";
|
||||
import Company from "../../../../services/user/entities/company";
|
||||
import Workspace from "../../../../services/workspaces/entities/workspace";
|
||||
import User from "../../../../services/user/entities/user";
|
||||
@@ -7,7 +6,6 @@ export type EmailBuilderDataPayload = {
|
||||
user: User;
|
||||
company: Company;
|
||||
notifications: {
|
||||
channel: Channel;
|
||||
workspace: Workspace;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
private getVisibilityObject(visibility: string) {
|
||||
return {
|
||||
is_direct: visibility === "direct" ? true : false,
|
||||
is_private: visibility === "private" ? true : false,
|
||||
is_public: visibility === "public" ? true : false,
|
||||
};
|
||||
}
|
||||
|
||||
public async identify(
|
||||
identity: IdentifyObjectType,
|
||||
callback?: (err: Error) => void,
|
||||
|
||||
@@ -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 WebSocketAPI from "../core/platform/services/websocket/provider";
|
||||
import TrackerAPI from "../core/platform/services/tracker/provider";
|
||||
import KnowledgeGraphService from "../core/platform/services/knowledge-graph";
|
||||
import EmailPusherAPI from "../core/platform/services/email-pusher/provider";
|
||||
|
||||
import { logger } from "../core/platform/framework";
|
||||
@@ -22,26 +21,14 @@ import assert from "assert";
|
||||
import { CompanyServiceImpl } from "./user/services/companies";
|
||||
import { WorkspaceServiceImpl } from "./workspaces/services/workspace";
|
||||
import { UserExternalLinksServiceImpl } from "./user/services/external_links";
|
||||
import { UserNotificationBadgeService } from "./notifications/services/bages";
|
||||
import { NotificationPreferencesService } from "./notifications/services/preferences";
|
||||
import { UserServiceImpl } from "./user/services/users/service";
|
||||
import { CompanyApplicationServiceImpl } from "./applications/services/company-applications";
|
||||
import { ApplicationServiceImpl } from "./applications/services/applications";
|
||||
import { FileServiceImpl } from "./files/services";
|
||||
import { ChannelServiceImpl } from "./channels/services/channel/service";
|
||||
import { MemberServiceImpl } from "./channels/services/member/service";
|
||||
import ChannelPendingEmailServiceImpl from "./channels/services/channel/pending-emails/service";
|
||||
import { TabServiceImpl } from "./channels/services/tab";
|
||||
import { ConsoleServiceImpl } from "./console/service";
|
||||
import { StatisticsServiceImpl } from "./statistics/service";
|
||||
import { NotificationEngine } from "./notifications/services/engine";
|
||||
import { MobilePushService } from "./notifications/services/mobile-push";
|
||||
import { ChannelMemberPreferencesServiceImpl } from "./notifications/services/channel-preferences";
|
||||
import { ChannelThreadUsersServiceImpl } from "./notifications/services/channel-thread-users";
|
||||
import { ApplicationHooksService } from "./applications/services/hooks";
|
||||
import OnlineServiceImpl from "./online/service";
|
||||
import { ChannelsMessageQueueListener } from "./channels/services/pubsub";
|
||||
import { UserNotificationDigestService } from "./notifications/services/digest";
|
||||
import { DocumentsService } from "./documents/services";
|
||||
import { DocumentsEngine } from "./documents/services/engine";
|
||||
import { TagsService } from "./tags/services/tags";
|
||||
@@ -58,7 +45,6 @@ type PlatformServices = {
|
||||
tracker: TrackerAPI;
|
||||
webserver: WebServerAPI;
|
||||
websocket: WebSocketAPI;
|
||||
knowledgeGraph: KnowledgeGraphService;
|
||||
emailPusher: EmailPusherAPI;
|
||||
};
|
||||
|
||||
@@ -69,28 +55,12 @@ type TdriveServices = {
|
||||
console: ConsoleServiceImpl;
|
||||
statistics: StatisticsServiceImpl;
|
||||
externalUser: UserExternalLinksServiceImpl;
|
||||
notifications: {
|
||||
badges: UserNotificationBadgeService;
|
||||
channelPreferences: ChannelMemberPreferencesServiceImpl;
|
||||
channelThreads: ChannelThreadUsersServiceImpl;
|
||||
engine: NotificationEngine;
|
||||
preferences: NotificationPreferencesService;
|
||||
mobilePush: MobilePushService;
|
||||
digest: UserNotificationDigestService;
|
||||
};
|
||||
applications: {
|
||||
marketplaceApps: ApplicationServiceImpl;
|
||||
companyApps: CompanyApplicationServiceImpl;
|
||||
hooks: ApplicationHooksService;
|
||||
};
|
||||
files: FileServiceImpl;
|
||||
channels: {
|
||||
channels: ChannelServiceImpl;
|
||||
members: MemberServiceImpl;
|
||||
pubsub: ChannelsMessageQueueListener;
|
||||
};
|
||||
channelPendingEmail: ChannelPendingEmailServiceImpl;
|
||||
tab: TabServiceImpl;
|
||||
online: OnlineServiceImpl;
|
||||
documents: {
|
||||
documents: DocumentsService;
|
||||
@@ -126,7 +96,6 @@ class GlobalResolver {
|
||||
tracker: platform.getProvider<TrackerAPI>("tracker"),
|
||||
webserver: platform.getProvider<WebServerAPI>("webserver"),
|
||||
websocket: platform.getProvider<WebSocketAPI>("websocket"),
|
||||
knowledgeGraph: await new KnowledgeGraphService().init(),
|
||||
emailPusher: platform.getProvider<EmailPusherAPI>("email-pusher"),
|
||||
};
|
||||
|
||||
@@ -144,28 +113,12 @@ class GlobalResolver {
|
||||
console: await new ConsoleServiceImpl().init(),
|
||||
statistics: await new StatisticsServiceImpl().init(),
|
||||
externalUser: await new UserExternalLinksServiceImpl().init(),
|
||||
notifications: {
|
||||
badges: await new UserNotificationBadgeService().init(platform),
|
||||
channelPreferences: await new ChannelMemberPreferencesServiceImpl().init(),
|
||||
channelThreads: await new ChannelThreadUsersServiceImpl().init(),
|
||||
engine: await new NotificationEngine().init(),
|
||||
preferences: await new NotificationPreferencesService().init(),
|
||||
mobilePush: await new MobilePushService().init(),
|
||||
digest: await new UserNotificationDigestService().init(),
|
||||
},
|
||||
applications: {
|
||||
marketplaceApps: await new ApplicationServiceImpl().init(),
|
||||
companyApps: await new CompanyApplicationServiceImpl().init(),
|
||||
hooks: await new ApplicationHooksService().init(),
|
||||
},
|
||||
files: await new FileServiceImpl().init(),
|
||||
channels: {
|
||||
channels: await new ChannelServiceImpl().init(),
|
||||
members: await new MemberServiceImpl().init(),
|
||||
pubsub: await new ChannelsMessageQueueListener().init(),
|
||||
},
|
||||
channelPendingEmail: await new ChannelPendingEmailServiceImpl().init(),
|
||||
tab: await new TabServiceImpl().init(),
|
||||
online: await new OnlineServiceImpl().init(),
|
||||
documents: {
|
||||
documents: await new DocumentsService().init(),
|
||||
|
||||
@@ -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 gr from "../../global-resolver";
|
||||
import { localEventBus } from "../../../core/platform/framework/event-bus";
|
||||
import {
|
||||
KnowledgeGraphEvents,
|
||||
KnowledgeGraphGenericEventPayload,
|
||||
} from "../../../core/platform/services/knowledge-graph/types";
|
||||
|
||||
export class CompanyServiceImpl {
|
||||
version: "1";
|
||||
@@ -100,15 +96,6 @@ export class CompanyServiceImpl {
|
||||
await this.externalCompanyRepository.save(extCompany, context);
|
||||
}
|
||||
|
||||
localEventBus.publish<KnowledgeGraphGenericEventPayload<Company>>(
|
||||
KnowledgeGraphEvents.COMPANY_UPSERT,
|
||||
{
|
||||
id: company.id,
|
||||
resource: company,
|
||||
links: [],
|
||||
},
|
||||
);
|
||||
|
||||
return new SaveResult<Company>("company", company, OperationType.UPDATE);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,6 @@ import { RealtimeSaved } from "../../../../core/platform/framework";
|
||||
import { getPublicUserRoom, getUserRoom } from "../../realtime";
|
||||
import NodeCache from "node-cache";
|
||||
import gr from "../../../global-resolver";
|
||||
import {
|
||||
KnowledgeGraphEvents,
|
||||
KnowledgeGraphGenericEventPayload,
|
||||
} from "../../../../core/platform/services/knowledge-graph/types";
|
||||
import { formatUser } from "../../../../utils/users";
|
||||
|
||||
export class UserServiceImpl {
|
||||
@@ -121,27 +117,11 @@ export class UserServiceImpl {
|
||||
},
|
||||
];
|
||||
})
|
||||
async save<SaveOptions>(user: User, context?: ExecutionContext): Promise<SaveResult<User>> {
|
||||
async save(user: User, context?: ExecutionContext): Promise<SaveResult<User>> {
|
||||
this.assignDefaults(user);
|
||||
await this.repository.save(user, context);
|
||||
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);
|
||||
|
||||
return new SaveResult("user", user, OperationType.UPDATE);
|
||||
|
||||
@@ -35,7 +35,6 @@ import { getCompanyRooms, getUserRooms } from "../realtime";
|
||||
import { formatCompany, getCompanyStats } from "../utils";
|
||||
import { formatUser } from "../../../utils/users";
|
||||
import gr from "../../global-resolver";
|
||||
import { UserChannel, UsersIncludedChannel } from "../../channels/entities";
|
||||
|
||||
export class UsersCrudController
|
||||
implements
|
||||
@@ -282,39 +281,11 @@ export class UsersCrudController
|
||||
}
|
||||
|
||||
async recent(
|
||||
request: FastifyRequest<{ Params: CompanyParameters; Querystring: { limit: 100 } }>,
|
||||
_request: FastifyRequest<{ Params: CompanyParameters; Querystring: { limit: 100 } }>,
|
||||
_reply: FastifyReply,
|
||||
): Promise<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 {
|
||||
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 { logger } from "@sentry/utils";
|
||||
import { localEventBus } from "../../../core/platform/framework/event-bus";
|
||||
import {
|
||||
KnowledgeGraphEvents,
|
||||
KnowledgeGraphGenericEventPayload,
|
||||
} from "../../../core/platform/services/knowledge-graph/types";
|
||||
import WorkspaceInviteDomain, {
|
||||
TYPE as WorkspaceInviteDomainType,
|
||||
getInstance as getWorkspaceInviteDomainInstance,
|
||||
@@ -239,15 +235,6 @@ export class WorkspaceServiceImpl implements TdriveServiceProvider, Initializabl
|
||||
});
|
||||
}
|
||||
|
||||
localEventBus.publish<KnowledgeGraphGenericEventPayload<Workspace>>(
|
||||
KnowledgeGraphEvents.WORKSPACE_UPSERT,
|
||||
{
|
||||
id: workspace.id,
|
||||
resource: workspace,
|
||||
links: [{ relation: "parent", type: "company", id: workspace.company_id }],
|
||||
},
|
||||
);
|
||||
|
||||
return new SaveResult<Workspace>(
|
||||
TYPE,
|
||||
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.workspace.id = workspace.id;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import WorkspacePendingUser from "../../entities/workspace_pending_users";
|
||||
import { ConsoleCompany, CreateConsoleUser } from "../../../console/types";
|
||||
import { hasCompanyAdminLevel } from "../../../../utils/company";
|
||||
import gr from "../../../global-resolver";
|
||||
import { getChannelPendingEmailsInstance } from "../../../channels/entities";
|
||||
|
||||
export class WorkspaceUsersCrudController
|
||||
implements
|
||||
@@ -413,16 +412,6 @@ export class WorkspaceUsersCrudController
|
||||
invitation.role,
|
||||
invitation.company_role,
|
||||
);
|
||||
(request.body.channels || []).forEach(async channelId => {
|
||||
const channelPendingEmail = getChannelPendingEmailsInstance({
|
||||
channel_id: channelId,
|
||||
company_id: request.params.company_id,
|
||||
email: invitation.email,
|
||||
workspace_id: context.workspace_id,
|
||||
});
|
||||
|
||||
await gr.services.channelPendingEmail.create(channelPendingEmail, context);
|
||||
});
|
||||
responses.push({ email: invitation.email, status: "ok" });
|
||||
};
|
||||
|
||||
@@ -516,18 +505,6 @@ export class WorkspaceUsersCrudController
|
||||
await Promise.all(
|
||||
usersToProcessImmediately.map(user => {
|
||||
gr.services.console.processPendingUser(user);
|
||||
gr.services.channelPendingEmail.proccessPendingEmails(
|
||||
{
|
||||
company_id: context.company_id,
|
||||
user_id: user.id,
|
||||
workspace_id: context.workspace_id,
|
||||
},
|
||||
{
|
||||
workspace_id: context.workspace_id,
|
||||
company_id: context.company_id,
|
||||
},
|
||||
context,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -3,13 +3,6 @@
|
||||
*/
|
||||
|
||||
import { WorkspacePrimaryKey } from "../services/workspaces/entities/workspace";
|
||||
import {
|
||||
Channel as ChannelEntity,
|
||||
ChannelMember,
|
||||
ChannelPendingEmails,
|
||||
ChannelTab,
|
||||
} from "../services/channels/entities";
|
||||
import { ChannelParameters } from "../services/channels/web/types";
|
||||
|
||||
export type uuid = string;
|
||||
|
||||
@@ -102,14 +95,9 @@ export interface ResourceWebsocket {
|
||||
|
||||
export interface ResourceEventsPayload {
|
||||
user: User;
|
||||
channel?: ChannelEntity;
|
||||
channelParameters?: ChannelParameters;
|
||||
guest?: ChannelPendingEmails;
|
||||
member?: ChannelMember;
|
||||
actor?: User;
|
||||
resourcesBefore?: (User | ChannelEntity | ChannelTab | ChannelMember)[];
|
||||
resourcesAfter?: (User | ChannelEntity | ChannelTab | ChannelMember)[];
|
||||
tab?: ChannelTab;
|
||||
resourcesBefore?: User[];
|
||||
resourcesAfter?: User[];
|
||||
company?: { id: string };
|
||||
workspace?: WorkspacePrimaryKey;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
buildIn,
|
||||
} from "../../../../../../../../../src/core/platform/services/database/services/orm/connectors/cassandra/query-builder";
|
||||
import { ChannelMemberNotificationPreference } from "../../../../../../../../../src/services/notifications/entities/channel-member-notification-preferences";
|
||||
import { DriveFile } from "../../../../../../../../../src/services/documents/entities/drive-file";
|
||||
|
||||
describe("The QueryBuilder module", () => {
|
||||
describe("The buildSelectQuery function", () => {
|
||||
it("should build a valid query from primary key parameters", () => {
|
||||
const filters = {
|
||||
company_id: "comp1",
|
||||
channel_id: "chan1",
|
||||
parent_id: "parent1",
|
||||
};
|
||||
const result = buildSelectQuery<ChannelMemberNotificationPreference>(
|
||||
ChannelMemberNotificationPreference,
|
||||
filters,
|
||||
{},
|
||||
{ keyspace: "tdrive" },
|
||||
);
|
||||
const result = buildSelectQuery<DriveFile>(DriveFile, filters, {}, { keyspace: "tdrive" });
|
||||
|
||||
expect(result).toEqual(
|
||||
"SELECT * FROM tdrive.channel_members_notification_preferences WHERE company_id = comp1 AND channel_id = chan1;",
|
||||
"SELECT * FROM tdrive.drive_files WHERE company_id = comp1 AND parent_id = 'parent1';",
|
||||
);
|
||||
});
|
||||
|
||||
it("should build a valid query from primary key parameters and comparison", () => {
|
||||
const filters = {
|
||||
company_id: "comp1",
|
||||
channel_id: "chan1",
|
||||
parent_id: "parent1",
|
||||
};
|
||||
const result = buildSelectQuery<ChannelMemberNotificationPreference>(
|
||||
ChannelMemberNotificationPreference,
|
||||
const result = buildSelectQuery<DriveFile>(
|
||||
DriveFile,
|
||||
filters,
|
||||
{
|
||||
$lt: [["last_read", 1000]],
|
||||
$lt: [["size", 1000]],
|
||||
},
|
||||
{ keyspace: "tdrive" },
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
"SELECT * FROM tdrive.channel_members_notification_preferences WHERE company_id = comp1 AND channel_id = chan1 AND last_read < 1000;",
|
||||
"SELECT * FROM tdrive.drive_files WHERE company_id = comp1 AND parent_id = 'parent1' AND size < 1000;",
|
||||
);
|
||||
});
|
||||
|
||||
it("should build IN query from array parameters", () => {
|
||||
const filters = {
|
||||
company_id: "comp1",
|
||||
channel_id: "chan1",
|
||||
user_id: ["u1", "u2", "u3"],
|
||||
parent_id: "parent1",
|
||||
creator: ["u1", "u2", "u3"],
|
||||
};
|
||||
const result = buildSelectQuery<ChannelMemberNotificationPreference>(
|
||||
ChannelMemberNotificationPreference,
|
||||
filters,
|
||||
{},
|
||||
{ keyspace: "tdrive" },
|
||||
);
|
||||
const result = buildSelectQuery<DriveFile>(DriveFile, filters, {}, { keyspace: "tdrive" });
|
||||
|
||||
expect(result).toEqual(
|
||||
"SELECT * FROM tdrive.channel_members_notification_preferences WHERE company_id = comp1 AND channel_id = chan1 AND user_id IN (u1,u2,u3);",
|
||||
"SELECT * FROM tdrive.drive_files WHERE company_id = comp1 AND parent_id = 'parent1' AND creator IN (u1,u2,u3);",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not build IN query from array parameters when array is empty", () => {
|
||||
const filters = {
|
||||
company_id: "comp1",
|
||||
channel_id: "chan1",
|
||||
parent_id: "parent1",
|
||||
user_id: [],
|
||||
};
|
||||
const result = buildSelectQuery<ChannelMemberNotificationPreference>(
|
||||
ChannelMemberNotificationPreference,
|
||||
filters,
|
||||
{},
|
||||
{ keyspace: "tdrive" },
|
||||
);
|
||||
const result = buildSelectQuery<DriveFile>(DriveFile, filters, {}, { keyspace: "tdrive" });
|
||||
|
||||
expect(result).toEqual(
|
||||
"SELECT * FROM tdrive.channel_members_notification_preferences WHERE company_id = comp1 AND channel_id = chan1;",
|
||||
"SELECT * FROM tdrive.drive_files WHERE company_id = comp1 AND parent_id = 'parent1';",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user