diff --git a/apps/web/public/static/images/providers/dark/rocket-chat.svg b/apps/web/public/static/images/providers/dark/rocket-chat.svg new file mode 100644 index 00000000000..8c1fd745bb9 --- /dev/null +++ b/apps/web/public/static/images/providers/dark/rocket-chat.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/web/public/static/images/providers/dark/square/rocket-chat.svg b/apps/web/public/static/images/providers/dark/square/rocket-chat.svg new file mode 100644 index 00000000000..8c1fd745bb9 --- /dev/null +++ b/apps/web/public/static/images/providers/dark/square/rocket-chat.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/web/public/static/images/providers/light/rocket-chat.svg b/apps/web/public/static/images/providers/light/rocket-chat.svg new file mode 100644 index 00000000000..8c1fd745bb9 --- /dev/null +++ b/apps/web/public/static/images/providers/light/rocket-chat.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/web/public/static/images/providers/light/square/rocket-chat.svg b/apps/web/public/static/images/providers/light/square/rocket-chat.svg new file mode 100644 index 00000000000..8c1fd745bb9 --- /dev/null +++ b/apps/web/public/static/images/providers/light/square/rocket-chat.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/libs/shared/src/consts/providers/channels/chat.ts b/libs/shared/src/consts/providers/channels/chat.ts index 5234c9c783b..9d354dd6536 100644 --- a/libs/shared/src/consts/providers/channels/chat.ts +++ b/libs/shared/src/consts/providers/channels/chat.ts @@ -1,5 +1,5 @@ import { IConfigCredentials, IProviderConfig } from '../provider.interface'; -import { grafanaOnCallConfig, slackConfig, getstreamConfig } from '../credentials'; +import { grafanaOnCallConfig, slackConfig, getstreamConfig, rocketChatConfig } from '../credentials'; import { ChatProviderIdEnum } from '../provider.enum'; @@ -70,4 +70,12 @@ export const chatProviders: IProviderConfig[] = [ docReference: 'https://getstream.io/chat/docs/node/?language=javascript', logoFileName: { light: 'getstream.svg', dark: 'getstream.svg' }, }, + { + id: ChatProviderIdEnum.RocketChat, + displayName: 'Rocket.Chat', + channel: ChannelTypeEnum.CHAT, + credentials: rocketChatConfig, + docReference: 'https://developer.rocket.chat/reference/api/rest-api/endpoints', + logoFileName: { light: 'rocket-chat.svg', dark: 'rocket-chat.svg' }, + }, ]; diff --git a/libs/shared/src/consts/providers/credentials/provider-credentials.ts b/libs/shared/src/consts/providers/credentials/provider-credentials.ts index 3b5a4a9cdb4..0ef8ac4673b 100644 --- a/libs/shared/src/consts/providers/credentials/provider-credentials.ts +++ b/libs/shared/src/consts/providers/credentials/provider-credentials.ts @@ -1046,3 +1046,20 @@ export const azureSmsConfig: IConfigCredentials[] = [ }, ...smsConfigBase, ]; + +export const rocketChatConfig: IConfigCredentials[] = [ + { + key: CredentialsKeyEnum.Token, + displayName: 'Personal Access Token (x-auth-token)', + description: 'Personal Access Token of your user', + type: 'text', + required: true, + }, + { + key: CredentialsKeyEnum.User, + displayName: 'User id (x-user-id)', + description: 'Your User id', + type: 'text', + required: true, + }, +]; diff --git a/libs/shared/src/consts/providers/provider.enum.ts b/libs/shared/src/consts/providers/provider.enum.ts index edad4da36e3..a83d61911ab 100644 --- a/libs/shared/src/consts/providers/provider.enum.ts +++ b/libs/shared/src/consts/providers/provider.enum.ts @@ -109,6 +109,7 @@ export enum ChatProviderIdEnum { Zulip = 'zulip', GrafanaOnCall = 'grafana-on-call', GetStream = 'getstream', + RocketChat = 'rocket-chat', } export enum PushProviderIdEnum { diff --git a/packages/application-generic/package.json b/packages/application-generic/package.json index c276de0a0bc..a185a4eec5c 100644 --- a/packages/application-generic/package.json +++ b/packages/application-generic/package.json @@ -114,6 +114,7 @@ "@novu/twilio": "^0.22.0", "@novu/zulip": "^0.22.0", "@novu/nexmo": "^0.22.0", + "@novu/rocket-chat": "^0.22.0", "@sentry/node": "^7.12.1", "@segment/analytics-node": "^1.1.4", "axios": "^1.6.2", diff --git a/packages/application-generic/src/factories/chat/chat.factory.ts b/packages/application-generic/src/factories/chat/chat.factory.ts index 957afccada3..ca365353e51 100644 --- a/packages/application-generic/src/factories/chat/chat.factory.ts +++ b/packages/application-generic/src/factories/chat/chat.factory.ts @@ -8,6 +8,7 @@ import { GrafanaOnCallHandler } from './handlers/grafana-on-call.handler'; import { RyverHandler } from './handlers/ryver.handler'; import { ZulipHandler } from './handlers/zulip.handler'; import { GetstreamChatHandler } from './handlers/getstream.handler'; +import { RocketChatHandler } from './handlers/rocket-chat.handler'; export class ChatFactory implements IChatFactory { handlers: IChatHandler[] = [ @@ -19,6 +20,7 @@ export class ChatFactory implements IChatFactory { new ZulipHandler(), new GrafanaOnCallHandler(), new GetstreamChatHandler(), + new RocketChatHandler(), ]; getHandler(integration: IntegrationEntity) { diff --git a/packages/application-generic/src/factories/chat/handlers/rocket-chat.handler.ts b/packages/application-generic/src/factories/chat/handlers/rocket-chat.handler.ts new file mode 100644 index 00000000000..e31b47aee5b --- /dev/null +++ b/packages/application-generic/src/factories/chat/handlers/rocket-chat.handler.ts @@ -0,0 +1,18 @@ +import { ICredentials, ChatProviderIdEnum } from '@novu/shared'; +import { ChannelTypeEnum } from '@novu/stateless'; +import { BaseChatHandler } from './base.handler'; +import { RocketChatProvider } from '@novu/rocket-chat'; + +export class RocketChatHandler extends BaseChatHandler { + constructor() { + super(ChatProviderIdEnum.RocketChat, ChannelTypeEnum.CHAT); + } + + buildProvider(credentials: ICredentials) { + const config: { token: string; user: string } = { + token: credentials.token as string, + user: credentials.user as string, + }; + this.provider = new RocketChatProvider(config); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbd9d0a143f..fe597dd94a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2600,6 +2600,9 @@ importers: '@novu/resend': specifier: ^0.22.0 version: link:../../providers/resend + '@novu/rocket-chat': + specifier: ^0.22.0 + version: link:../../providers/rocket-chat '@novu/ryver': specifier: ^0.22.0 version: link:../../providers/ryver @@ -5671,6 +5674,49 @@ importers: specifier: 4.9.5 version: 4.9.5 + providers/rocket-chat: + dependencies: + '@novu/stateless': + specifier: 0.16.3 + version: 0.16.3 + axios: + specifier: ^1.6.2 + version: 1.6.2 + devDependencies: + '@istanbuljs/nyc-config-typescript': + specifier: ~1.0.1 + version: 1.0.2(nyc@15.1.0) + '@types/jest': + specifier: ~27.5.2 + version: 27.5.2 + cspell: + specifier: ~6.19.2 + version: 6.19.2 + jest: + specifier: ~27.5.1 + version: 27.5.1(ts-node@10.9.1) + npm-run-all: + specifier: ^4.1.5 + version: 4.1.5 + nyc: + specifier: ~15.1.0 + version: 15.1.0 + prettier: + specifier: ~2.8.0 + version: 2.8.7 + rimraf: + specifier: ~3.0.2 + version: 3.0.2 + ts-jest: + specifier: ~27.1.5 + version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + ts-node: + specifier: ~10.9.1 + version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) + typescript: + specifier: 4.9.5 + version: 4.9.5 + providers/ryver: dependencies: '@novu/stateless': @@ -17100,6 +17146,15 @@ packages: vue-resize: 2.0.0-alpha.1(vue@3.2.47) dev: false + /@novu/stateless@0.16.3: + resolution: {integrity: sha512-9wrqUluSaR9rCdDp4oUWFBwsk6OSM4P+yMaEtFGHrGmSPMcspnvXQaQoARKakuUpCXFvQlPPJq5+yaicv4o/hA==} + engines: {node: '>=10'} + dependencies: + handlebars: 4.7.7 + lodash.get: 4.4.2 + lodash.merge: 4.6.2 + dev: false + /@npmcli/arborist@5.3.0: resolution: {integrity: sha512-+rZ9zgL1lnbl8Xbb1NQdMjveOMwj4lIYfcDtyJHHi5x4X8jtR6m8SXooJMZy5vmFVZ8w7A2Bnd/oX9eTuU8w5A==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -49640,13 +49695,13 @@ packages: uglify-js: optional: true dependencies: - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/trace-mapping': 0.3.18 '@swc/core': 1.3.49 esbuild: 0.18.20 jest-worker: 27.5.1 - schema-utils: 3.3.0 + schema-utils: 3.1.2 serialize-javascript: 6.0.1 - terser: 5.22.0 + terser: 5.16.9 webpack: 5.78.0(@swc/core@1.3.49)(esbuild@0.18.20)(webpack-cli@5.1.4) /terser-webpack-plugin@5.3.9(@swc/core@1.3.49)(esbuild@0.18.20)(webpack@5.82.1): @@ -49690,12 +49745,12 @@ packages: uglify-js: optional: true dependencies: - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/trace-mapping': 0.3.18 esbuild: 0.18.17 jest-worker: 27.5.1 - schema-utils: 3.3.0 + schema-utils: 3.1.2 serialize-javascript: 6.0.1 - terser: 5.22.0 + terser: 5.16.9 webpack: 5.88.2(esbuild@0.18.17) dev: true @@ -49715,11 +49770,11 @@ packages: uglify-js: optional: true dependencies: - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/trace-mapping': 0.3.18 jest-worker: 27.5.1 - schema-utils: 3.3.0 + schema-utils: 3.1.2 serialize-javascript: 6.0.1 - terser: 5.22.0 + terser: 5.16.9 webpack: 5.78.0 dev: true diff --git a/providers/rocket-chat/.czrc b/providers/rocket-chat/.czrc new file mode 100644 index 00000000000..d1bcc209ca1 --- /dev/null +++ b/providers/rocket-chat/.czrc @@ -0,0 +1,3 @@ +{ + "path": "cz-conventional-changelog" +} diff --git a/providers/rocket-chat/.eslintrc.json b/providers/rocket-chat/.eslintrc.json new file mode 100644 index 00000000000..ec40100be69 --- /dev/null +++ b/providers/rocket-chat/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.js" +} diff --git a/providers/rocket-chat/.gitignore b/providers/rocket-chat/.gitignore new file mode 100644 index 00000000000..963d5292865 --- /dev/null +++ b/providers/rocket-chat/.gitignore @@ -0,0 +1,9 @@ +.idea/* +.nyc_output +build +node_modules +test +src/**.js +coverage +*.log +package-lock.json diff --git a/providers/rocket-chat/README.md b/providers/rocket-chat/README.md new file mode 100644 index 00000000000..ea2bbe8587b --- /dev/null +++ b/providers/rocket-chat/README.md @@ -0,0 +1,14 @@ +# Novu RocketChat Provider + +A RocketChat chat provider library for [@novu/node](https://github.com/novuhq/novu) + +## Usage + +```javascript +import { RocketChatProvider } from '@novu/rocket-chat'; + +const provider = new RocketChatProvider({ + user: process.env.ROCKET_CHAT_USER_ID, + token: process.env.ROCKET_CHAT_TOKEN, +}); +``` diff --git a/providers/rocket-chat/jest.config.js b/providers/rocket-chat/jest.config.js new file mode 100644 index 00000000000..61faa20934a --- /dev/null +++ b/providers/rocket-chat/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + moduleNameMapper: { + axios: 'axios/dist/node/axios.cjs', + }, +}; diff --git a/providers/rocket-chat/package.json b/providers/rocket-chat/package.json new file mode 100644 index 00000000000..7d2b8fab745 --- /dev/null +++ b/providers/rocket-chat/package.json @@ -0,0 +1,78 @@ +{ + "name": "@novu/rocket-chat", + "version": "0.22.0", + "description": "A rocket-chat wrapper for novu", + "main": "build/main/index.js", + "typings": "build/main/index.d.ts", + "module": "build/module/index.js", + "private": false, + "repository": "https://github.com/novuhq/novu", + "license": "MIT", + "keywords": [], + "scripts": { + "prebuild": "rimraf build", + "build": "run-p build:*", + "build:main": "tsc -p tsconfig.json", + "build:module": "tsc -p tsconfig.module.json", + "fix": "run-s fix:*", + "fix:prettier": "prettier \"src/**/*.ts\" --write", + "fix:lint": "eslint src --ext .ts --fix", + "test": "run-s test:*", + "lint": "eslint src --ext .ts", + "test:unit": "jest src", + "watch:build": "tsc -p tsconfig.json -w", + "watch:test": "jest src --watch", + "reset-hard": "git clean -dfx && git reset --hard && yarn", + "prepare-release": "run-s reset-hard test" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@novu/stateless": "0.16.3", + "axios": "^1.6.2" + }, + "devDependencies": { + "@istanbuljs/nyc-config-typescript": "~1.0.1", + "@types/jest": "~27.5.2", + "cspell": "~6.19.2", + "jest": "~27.5.1", + "npm-run-all": "^4.1.5", + "nyc": "~15.1.0", + "prettier": "~2.8.0", + "rimraf": "~3.0.2", + "ts-jest": "~27.1.5", + "ts-node": "~10.9.1", + "typescript": "4.9.5" + }, + "files": [ + "build/main", + "build/module", + "!**/*.spec.*", + "!**/*.json", + "CHANGELOG.md", + "LICENSE", + "README.md" + ], + "ava": { + "failFast": true, + "timeout": "60s", + "typescript": { + "rewritePaths": { + "src/": "build/main/" + } + }, + "files": [ + "!build/module/**" + ] + }, + "prettier": { + "singleQuote": true + }, + "nyc": { + "extends": "@istanbuljs/nyc-config-typescript", + "exclude": [ + "**/*.spec.js" + ] + } +} diff --git a/providers/rocket-chat/src/index.ts b/providers/rocket-chat/src/index.ts new file mode 100644 index 00000000000..65539af5399 --- /dev/null +++ b/providers/rocket-chat/src/index.ts @@ -0,0 +1 @@ +export * from './lib/rocket-chat.provider'; diff --git a/providers/rocket-chat/src/lib/rocket-chat.provider.spec.ts b/providers/rocket-chat/src/lib/rocket-chat.provider.spec.ts new file mode 100644 index 00000000000..d861f99f532 --- /dev/null +++ b/providers/rocket-chat/src/lib/rocket-chat.provider.spec.ts @@ -0,0 +1,29 @@ +import { RocketChatProvider } from './rocket-chat.provider'; + +test('should trigger rocket-chat library correctly', async () => { + const mockConfig = { + user: '', + token: '', + }; + const provider = new RocketChatProvider(mockConfig); + + const spy = jest + .spyOn(provider, 'sendMessage') + .mockImplementation(async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any; + }); + + await provider.sendMessage({ + webhookUrl: '', + channel: '', + content: '', + }); + + expect(spy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith({ + webhookUrl: '', + channel: '', + content: '', + }); +}); diff --git a/providers/rocket-chat/src/lib/rocket-chat.provider.ts b/providers/rocket-chat/src/lib/rocket-chat.provider.ts new file mode 100644 index 00000000000..0ef276e348c --- /dev/null +++ b/providers/rocket-chat/src/lib/rocket-chat.provider.ts @@ -0,0 +1,46 @@ +import { + ChannelTypeEnum, + ISendMessageSuccessResponse, + IChatOptions, + IChatProvider, +} from '@novu/stateless'; +import axios from 'axios'; + +export class RocketChatProvider implements IChatProvider { + id = 'rocket-chat'; + channelType = ChannelTypeEnum.CHAT as ChannelTypeEnum.CHAT; + private axiosInstance = axios.create(); + + constructor( + private config: { + token: string; + user: string; + } + ) {} + + async sendMessage( + options: IChatOptions + ): Promise { + const roomId = options.channel; + const payload = { + message: { + rid: roomId, + msg: options.content, + }, + }; + const headers = { + 'x-auth-token': this.config.token, + 'x-user-id': this.config.user, + 'Content-Type': 'application/json', + }; + const baseURL = `${options.webhookUrl.toString()}/api/v1/chat.sendMessage`; + const { data } = await this.axiosInstance.post(baseURL, payload, { + headers: headers, + }); + + return { + id: data.message._id, + date: data.message.ts, + }; + } +} diff --git a/providers/rocket-chat/tsconfig.json b/providers/rocket-chat/tsconfig.json new file mode 100644 index 00000000000..5b8120fea36 --- /dev/null +++ b/providers/rocket-chat/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "build/main", + "rootDir": "src", + "types": ["node", "jest"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules/**"] +} diff --git a/providers/rocket-chat/tsconfig.module.json b/providers/rocket-chat/tsconfig.module.json new file mode 100644 index 00000000000..79be3a5c40b --- /dev/null +++ b/providers/rocket-chat/tsconfig.module.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "target": "esnext", + "outDir": "build/module", + "module": "esnext" + }, + "exclude": ["node_modules/**"] +}