diff --git a/README.md b/README.md index 371a196e6..ce5b75322 100644 --- a/README.md +++ b/README.md @@ -95,13 +95,13 @@ Make sure that your `.env` configurations exists. Add new section to the `docker-compose.{dev,prod}.yml` files. ``` - hawk-worker-telegram: + hawk-worker-sender: image: "codexteamuser/hawk-workers:prod" env_file: - .env - - workers/telegram/.env + - workers/sender/.env restart: unless-stopped - entrypoint: /usr/local/bin/node runner.js hawk-worker-telegram + entrypoint: /usr/local/bin/node runner.js hawk-worker-sender ``` ## Error handling diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 29766b23d..5abb5134e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -121,51 +121,15 @@ services: - ./:/usr/src/app - workers-deps:/usr/src/app/node_modules - hawk-worker-email: + hawk-worker-sender: build: dockerfile: "dev.Dockerfile" context: . env_file: - .env - - workers/email/.env + - workers/sender/.env restart: unless-stopped - entrypoint: yarn run-email - volumes: - - ./:/usr/src/app - - workers-deps:/usr/src/app/node_modules - - hawk-worker-telegram: - build: - dockerfile: "dev.Dockerfile" - context: . - env_file: - - .env - restart: unless-stopped - entrypoint: yarn run-telegram - volumes: - - ./:/usr/src/app - - workers-deps:/usr/src/app/node_modules - - hawk-worker-slack: - build: - dockerfile: "dev.Dockerfile" - context: . - env_file: - - .env - restart: unless-stopped - entrypoint: yarn run-slack - volumes: - - ./:/usr/src/app - - workers-deps:/usr/src/app/node_modules - - hawk-worker-webhook: - build: - dockerfile: "dev.Dockerfile" - context: . - env_file: - - .env - restart: unless-stopped - entrypoint: yarn run-webhook + entrypoint: yarn run-sender volumes: - ./:/usr/src/app - workers-deps:/usr/src/app/node_modules diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 88a6e0fa3..a7bbc55bd 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -67,20 +67,11 @@ services: restart: unless-stopped entrypoint: /usr/local/bin/node runner.js hawk-worker-notifier - hawk-worker-email: + hawk-worker-sender: image: "codexteamuser/hawk-workers:prod" network_mode: host env_file: - .env - - workers/email/.env + - workers/sender/.env restart: unless-stopped - entrypoint: /usr/local/bin/node runner.js hawk-worker-email - - hawk-worker-telegram: - image: "codexteamuser/hawk-workers:prod" - network_mode: host - env_file: - - .env - - workers/telegram/.env - restart: unless-stopped - entrypoint: /usr/local/bin/node runner.js hawk-worker-telegram + entrypoint: /usr/local/bin/node runner.js hawk-worker-sender diff --git a/lib/workerNames.js b/lib/workerNames.js index 976e3266c..ba76e7542 100644 --- a/lib/workerNames.js +++ b/lib/workerNames.js @@ -37,4 +37,11 @@ workersDir.forEach(file => { workers[file.name.toUpperCase()] = pkg.workerType; }); +/** + * Queues of the sender worker channels (one queue per channel, see workers/sender) + */ +['email', 'telegram', 'slack', 'webhook', 'loop'].forEach(channel => { + workers[channel.toUpperCase()] = `sender/${channel}`; +}); + module.exports = workers; diff --git a/package.json b/package.json index f2e1e154d..f46e2f079 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,7 @@ "test:sentry": "jest workers/sentry --config workers/sentry/jest.config.js", "test:javascript": "jest workers/javascript", "test:release": "jest workers/release", - "test:slack": "jest workers/slack", - "test:loop": "jest workers/loop", + "test:sender": "jest workers/sender", "test:limiter": "jest workers/limiter --runInBand", "test:grouper": "jest workers/grouper", "test:diff": "jest ./workers/grouper/tests/diff.test.ts", @@ -34,24 +33,19 @@ "test:notifier": "jest workers/notifier", "test:js": "jest workers/javascript", "test:task-manager": "jest workers/task-manager", - "test:webhook": "jest workers/webhook", "test:clear": "jest --clearCache", "run-default": "yarn worker hawk-worker-default", "run-sentry": "yarn worker hawk-worker-sentry", "run-js": "yarn worker hawk-worker-javascript", - "run-slack": "yarn worker hawk-worker-slack", - "run-loop": "yarn worker hawk-worker-loop", + "run-sender": "yarn worker hawk-worker-sender", "run-grouper": "yarn worker hawk-worker-grouper", "run-archiver": "yarn worker hawk-worker-archiver", "run-accountant": "yarn worker hawk-worker-accountant", "run-paymaster": "yarn worker hawk-worker-paymaster", "run-notifier": "yarn worker hawk-worker-notifier", "run-release": "yarn worker hawk-worker-release", - "run-email": "yarn worker hawk-worker-email", - "run-telegram": "yarn worker hawk-worker-telegram", "run-limiter": "yarn worker hawk-worker-limiter", - "run-task-manager": "yarn worker hawk-worker-task-manager", - "run-webhook": "yarn worker hawk-worker-webhook" + "run-task-manager": "yarn worker hawk-worker-task-manager" }, "dependencies": { "@babel/parser": "^7.26.9", diff --git a/workers/email/.env.sample b/workers/email/.env.sample deleted file mode 100644 index b0222fac5..000000000 --- a/workers/email/.env.sample +++ /dev/null @@ -1,25 +0,0 @@ -# SMTP mail settings - -## SMTP server adress -SMTP_HOST=smtp.yandex.ru - -## SMTP server port -SMTP_PORT=465 - -## SMTP server username (for examle, from your Yandex account) -SMTP_USERNAME= - -## SMTP server password (for examle, from your Yandex account) -SMTP_PASSWORD= - -## Sender name -SMTP_SENDER_NAME= - -## Mail from which letters are sent -SMTP_SENDER_ADDRESS= - -## Garage url for email overview -GARAGE_URL=http://localhost:8080 - -## Static files url for email overview -API_STATIC_URL=http://localhost:4000/static \ No newline at end of file diff --git a/workers/email/.gitignore b/workers/email/.gitignore deleted file mode 100644 index 4c49bd78f..000000000 --- a/workers/email/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.env diff --git a/workers/email/README.md b/workers/email/README.md deleted file mode 100644 index d740d3b5e..000000000 --- a/workers/email/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Worker / Sender/Email - -Handle tasks from Notifier worker and send email letters with data about received events. - -## How to run - -1. Make sure you are in Workers root directory -3. `yarn install` -4. `yarn run-email` - -## When you will add new email template - -Don't forget to rebuild auto-generated ENUM with available templates: - -1. Go to `workers/email` directory -2. `yarn run generate-tpl-names` - -## How to debug email templates - -There is a special util allows to debug the email templates. - -1. Go to `workers/email` directory -2. `yarn run email-overview` -3. Open http://localhost:4444/ diff --git a/workers/email/package.json b/workers/email/package.json deleted file mode 100644 index 2fd272e1a..000000000 --- a/workers/email/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "hawk-worker-email", - "version": "0.0.1", - "main": "src/index.ts", - "license": "UNLICENSED", - "workerType": "sender/email", - "scripts": { - "generate-tpl-names": "cd scripts && ts-node generateTemplatesNames", - "email-overview": "cd scripts && ts-node emailOverview" - }, - "dependencies": { - "@types/dotenv": "^8.2.0", - "@types/twig": "^1.12.3", - "nodemailer": "^6.6.1", - "short-number": "^1.0.7", - "ts-node": "^8.8.2", - "twig": "^1.15.0" - }, - "devDependencies": { - "@types/nodemailer": "^6.4.0" - } -} diff --git a/workers/email/src/env.ts b/workers/email/src/env.ts deleted file mode 100644 index 2714c70b1..000000000 --- a/workers/email/src/env.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as dotenv from 'dotenv'; -import * as path from 'path'; - -/** - * Load local environment configuration - */ -const localEnv = dotenv.config({ path: path.resolve(__dirname, '../.env') }).parsed; - -Object.assign(process.env, localEnv); diff --git a/workers/email/src/index.ts b/workers/email/src/index.ts deleted file mode 100644 index ef27f7916..000000000 --- a/workers/email/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as pkg from '../package.json'; -import './env'; -import EmailProvider from './provider'; -import SenderWorker from 'hawk-worker-sender/src'; -import { ChannelType } from 'hawk-worker-notifier/types/channel'; - -/** - * Worker to send email notifications - */ -export default class EmailSenderWorker extends SenderWorker { - /** - * Worker type - */ - public readonly type: string = pkg.workerType; - - /** - * Email channel type - */ - protected channelType = ChannelType.Email; - - /** - * Email provider - */ - protected provider = new EmailProvider(); -} diff --git a/workers/email/yarn.lock b/workers/email/yarn.lock deleted file mode 100644 index 75f60c0fb..000000000 --- a/workers/email/yarn.lock +++ /dev/null @@ -1,155 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/runtime@^7.8.4": - version "7.15.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b" - integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA== - dependencies: - regenerator-runtime "^0.13.4" - -"@types/dotenv@^8.2.0": - version "8.2.0" - resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-8.2.0.tgz#5cd64710c3c98e82d9d15844375a33bf1b45d053" - integrity sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw== - dependencies: - dotenv "*" - -"@types/node@*": - version "13.11.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b" - integrity sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ== - -"@types/nodemailer@^6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.0.tgz#d8c039be3ed685c4719a026455555be82c124b74" - integrity sha512-KY7bFWB0MahRZvVW4CuW83qcCDny59pJJ0MQ5ifvfcjNwPlIT0vW4uARO4u1gtkYnWdhSvURegecY/tzcukJcA== - dependencies: - "@types/node" "*" - -"@types/twig@^1.12.3": - version "1.12.6" - resolved "https://registry.yarnpkg.com/@types/twig/-/twig-1.12.6.tgz#75a9eccedbc32fb838996aea2fe92792a88f8305" - integrity sha512-100EU17ElcksEPcmCe5J9+k+RwIMEqjKF4JuaBviFIY3liEt3T0KOc+d+6R+77yNvUV47ldZpInR1UryjbRK9A== - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -dotenv@*: - version "10.0.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" - integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== - -foreachasync@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" - integrity sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY= - -locutus@^2.0.11: - version "2.0.15" - resolved "https://registry.yarnpkg.com/locutus/-/locutus-2.0.15.tgz#d75b9100713aaaf30be8b38ed6543910498c0db0" - integrity sha512-2xWC4RkoAoCVXEb/stzEgG1TNgd+mrkLBj6TuEDNyUoKeQ2XzDTyJUC23sMiqbL6zJmJSP3w59OZo+zc4IBOmA== - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -minimatch@3.0.x: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -nodemailer@^6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.6.1.tgz#2a05fbf205b897d71bf43884167b5d4d3bd01b99" - integrity sha512-1xzFN3gqv+/qJ6YRyxBxfTYstLNt0FCtZaFRvf4Sg9wxNGWbwFmGXVpfSi6ThGK6aRxAo+KjHtYSW8NvCsNSAg== - -regenerator-runtime@^0.13.4: - version "0.13.9" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" - integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== - -short-number@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/short-number/-/short-number-1.0.7.tgz#5ab8021b86f20a4bdd1bbe01a85c22ff1cd15514" - integrity sha512-e3cb811QXHiAH2H5rgZwXHsmg2+aG6XK0QyKF/+pyDIZFBSz/G8bkr8avdBrL5PVIfhIq8rlohjFvSB3azzpEQ== - -source-map-support@^0.5.6: - version "0.5.16" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" - integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -ts-node@^8.8.2: - version "8.8.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.8.2.tgz#0b39e690bee39ea5111513a9d2bcdc0bc121755f" - integrity sha512-duVj6BpSpUpD/oM4MfhO98ozgkp3Gt9qIp3jGxwU2DFvl/3IRaEAvbLa8G60uS7C77457e/m5TMowjedeRxI1Q== - dependencies: - arg "^4.1.0" - diff "^4.0.1" - make-error "^1.1.1" - source-map-support "^0.5.6" - yn "3.1.1" - -twig@^1.15.0: - version "1.15.4" - resolved "https://registry.yarnpkg.com/twig/-/twig-1.15.4.tgz#314b00eaf98a57e70ed7dcfd6def93d0d8f3d627" - integrity sha512-gRpGrpdf+MswqF6eSjEdYZTa/jt3ZWHK/NU59IbTYJMBQXJ1W+7IxaGEwLkQjd+mNT15j9sQTzQumxUBkuQueQ== - dependencies: - "@babel/runtime" "^7.8.4" - locutus "^2.0.11" - minimatch "3.0.x" - walk "2.3.x" - -walk@2.3.x: - version "2.3.14" - resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.14.tgz#60ec8631cfd23276ae1e7363ce11d626452e1ef3" - integrity sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg== - dependencies: - foreachasync "^3.0.0" - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/workers/loop/package.json b/workers/loop/package.json deleted file mode 100644 index eccaaec7f..000000000 --- a/workers/loop/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "hawk-worker-loop", - "version": "1.0.0", - "description": "", - "main": "src/index.ts", - "license": "MIT", - "workerType": "sender/loop", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { - "@slack/webhook": "^5.0.3", - "json-templater": "^1.2.0" - } -} diff --git a/workers/loop/src/index.ts b/workers/loop/src/index.ts deleted file mode 100644 index 174a5ac37..000000000 --- a/workers/loop/src/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as pkg from './../package.json'; -import LoopProvider from './provider'; -import SenderWorker from 'hawk-worker-sender/src'; -import { ChannelType } from 'hawk-worker-notifier/types/channel'; - -/** - * Worker to send email notifications - */ -export default class LoopSenderWorker extends SenderWorker { - /** - * Worker type - */ - public readonly type: string = pkg.workerType; - - /** - * Email channel type - */ - protected channelType = ChannelType.Loop; - - /** - * Email provider - */ - protected provider = new LoopProvider(); -} diff --git a/workers/loop/yarn.lock b/workers/loop/yarn.lock deleted file mode 100644 index 1f72f556a..000000000 --- a/workers/loop/yarn.lock +++ /dev/null @@ -1,48 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@slack/types@^1.2.1": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@slack/types/-/types-1.5.0.tgz#5c2cb0f718689266ff295aad33301d489272c842" - integrity sha512-oCYgatJYxHf9wE3tKXzOLeeTsF0ghX1TIcguNfVmO2V6NDe+cHAzZRglEOmJLdRINDS5gscAgSkeZpDhpKBeUA== - -"@slack/webhook@^5.0.3": - version "5.0.3" - resolved "https://registry.yarnpkg.com/@slack/webhook/-/webhook-5.0.3.tgz#2205cba9a8d49d2ae84ca93f11ab4a1dba2f963b" - integrity sha512-51vnejJ2zABNumPVukOLyerpHQT39/Lt0TYFtOEz/N2X77bPofOgfPj2atB3etaM07mxWHLT9IRJ4Zuqx38DkQ== - dependencies: - "@slack/types" "^1.2.1" - "@types/node" ">=8.9.0" - axios "^0.19.0" - -"@types/node@>=8.9.0": - version "13.11.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b" - integrity sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ== - -axios@^0.19.0: - version "0.19.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" - integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== - dependencies: - follow-redirects "1.5.10" - -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== - dependencies: - debug "=3.1.0" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= diff --git a/workers/sender/.env.sample b/workers/sender/.env.sample index b6d9666f1..aa676f2b2 100644 --- a/workers/sender/.env.sample +++ b/workers/sender/.env.sample @@ -1,3 +1,7 @@ +# Comma-separated list of channels to serve: email,telegram,slack,webhook,loop +# All channels are enabled when not set +# SENDER_CHANNELS=email,telegram,slack,webhook,loop + # Garage URL will be used in email link for events, unsubscribe etc # -- without trailing slash -- GARAGE_URL=http://localhost:8080 @@ -5,3 +9,23 @@ GARAGE_URL=http://localhost:8080 # API URL will be used in email static files, such as icons and other # -- without trailing slash -- API_STATIC_URL=http://localhost:4000/static + +# SMTP mail settings (email channel) + +## SMTP server adress +SMTP_HOST=smtp.yandex.ru + +## SMTP server port +SMTP_PORT=465 + +## SMTP server username (for examle, from your Yandex account) +SMTP_USERNAME= + +## SMTP server password (for examle, from your Yandex account) +SMTP_PASSWORD= + +## Sender name +SMTP_SENDER_NAME= + +## Mail from which letters are sent +SMTP_SENDER_ADDRESS= diff --git a/workers/sender/README.md b/workers/sender/README.md index 87f81baba..49c2b9813 100644 --- a/workers/sender/README.md +++ b/workers/sender/README.md @@ -1,82 +1,70 @@ -# Sender worker / Abstract 🧰 +# Sender worker 📮 -This worker provides abstract classes for implementing notify-senders -for different channels. For example, Email, Telegram, Slack notifiers. +Multi-channel worker that delivers notifications to users. One process serves +all notification channels: `email`, `telegram`, `slack`, `webhook`, `loop`. -## How to implement a new worker for a specific channel +Each enabled channel runs its own consumer on the `sender/` queue, so +per-channel backpressure and retries are preserved, while MongoDB connections +are shared between channels. -1. Create a class that implements abstract sender. +## Configuration -```ts -import * as pkg from '../package.json'; -import NewProvider from './provider'; -import SenderWorker from 'hawk-worker-sender/src'; -import { ChannelType } from 'hawk-worker-notifier/types/channel'; - -/** - * Worker to send email notifications - */ -export default class NewSenderWorker extends SenderWorker { - /** - * Worker type - */ - public readonly type: string = pkg.workerType; - - /** - * Email channel type - */ - protected channelType = ChannelType.NewType; - - /** - * Email provider - */ - protected provider = new NewProvider(); -} -``` +Besides common worker variables (see the root `.env`), the sender uses: -2. Implement a provider that will render and send messages +| Variable | Description | +| -- | -- | +| `SENDER_CHANNELS` | Comma-separated list of channels to serve (e.g. `email,telegram`). All channels are enabled when not set. | +| `GARAGE_URL` | Garage URL used in notification links | +| `API_STATIC_URL` | API static files URL (icons etc.) | +| `SMTP_*` | SMTP settings for the email channel (see `.env.sample`) | -```ts -import { TemplateVariables } from 'hawk-worker-sender/types/template-variables'; -import templates, { Template } from './templates'; -import Templates from './templates/names'; - -/** - * Class to provide rendering and transport - */ -export default class NewProvider extends TemplateVariables { - /** - * Send email to recipient - * - * @param {string} to - recipient endpoint - * @param {TemplateVariables} variables - variables for template - */ - public async send(to: string, variables: Template): Promise { - // logic for rendering and sending - } -} -``` +## Run -## How to implement a new template +```bash +yarn run-sender +``` -1. Create a new type for task and payload in `workers/sender/src/types/sender-task`. +## Structure -2. Create a new type for notification in `workers/sender/src/types/template-variables`. +- `src/index.ts` — multi-channel worker: reads `SENDER_CHANNELS`, owns db connections, starts channel workers. +- `src/channel-sender.ts` — worker of a single channel: consumes `sender/` queue, handles tasks, calls the provider. +- `src/channels.ts` — registry of channels and their providers. +- `src/providers//` — provider (rendering + delivery) and templates of each channel. +- `types/sender-task/` — task payload types (used by producers: notifier, paymaster, api). +- `types/template-variables/` — notification template variables types. -3. Create a new case for switch in `SenderWorker.handle()` method. +## How to add a new channel -4. Create a new handler method in `SenderWorker` class. +1. Add the channel to `ChannelType` (`workers/notifier/types/channel.ts`). +2. Create `src/providers//provider.ts` extending `NotificationsProvider` — it renders and sends messages: -Let's create a template for Email worker for example. +```ts +import NotificationsProvider from '../../provider'; +import { Notification } from '../../../types/template-variables'; -5. Go to `workers/email/src/emails` and create a new directory for templates. +export default class NewProvider extends NotificationsProvider { + public async send(to: string, notification: Notification): Promise { + // rendering and delivery logic + } +} +``` -6. Go to `workers/email` and update `names` file by the following command: +3. Register it in `src/channels.ts`. +4. Declare the `sender/` queue in hawk.registry definitions. -`yarn generate-tpl-names` +## How to implement a new notification type -7. Then go provider's switch in `EmailProvider.send` method which resolves template and add a new one. +1. Create a new type for task and payload in `types/sender-task`. +2. Create a new type for notification in `types/template-variables`. +3. Create a new case for switch in `ChannelSenderWorker.handle()` method. +4. Create a new handler method in `ChannelSenderWorker` class. +5. Add templates to the channels that should support it (e.g. for email: + create a directory in `src/providers/email/templates/emails` and run + `yarn generate-tpl-names`, then add a case to `EmailProvider.send`). -Now you can test it by adding new tasks with a new name type. +## Email templates preview -Good luck. +```bash +yarn email-overview +# open http://localhost:4444/ +``` diff --git a/workers/sender/package.json b/workers/sender/package.json index 9fcbd9592..eca04c6e1 100644 --- a/workers/sender/package.json +++ b/workers/sender/package.json @@ -3,5 +3,24 @@ "version": "0.0.1", "workerType": "sender", "main": "src/index.ts", - "license": "UNLICENSED" + "license": "UNLICENSED", + "scripts": { + "generate-tpl-names": "cd scripts && ts-node generateTemplatesNames", + "email-overview": "cd scripts && ts-node emailOverview" + }, + "dependencies": { + "@slack/webhook": "^5.0.3", + "@types/dotenv": "^8.2.0", + "@types/twig": "^1.12.3", + "axios": "^0.21.2", + "json-templater": "^1.2.0", + "nodemailer": "^6.6.1", + "short-number": "^1.0.7", + "slack-block-kit": "^0.9.9", + "ts-node": "^8.8.2", + "twig": "^1.15.0" + }, + "devDependencies": { + "@types/nodemailer": "^6.4.0" + } } diff --git a/workers/email/scripts/emailOverview.ts b/workers/sender/scripts/emailOverview.ts similarity index 99% rename from workers/email/scripts/emailOverview.ts rename to workers/sender/scripts/emailOverview.ts index a108b86b8..7d05653b9 100644 --- a/workers/email/scripts/emailOverview.ts +++ b/workers/sender/scripts/emailOverview.ts @@ -9,7 +9,7 @@ */ import * as http from 'http'; import * as url from 'url'; -import templates, { Template } from '../src/templates'; +import templates, { Template } from '../src/providers/email/templates'; import type { TemplateVariables, TemplateEventData } from 'hawk-worker-sender/types/template-variables'; import * as Twig from 'twig'; import { DatabaseController } from '../../../lib/db/controller'; diff --git a/workers/email/scripts/emailOverviewForm.twig b/workers/sender/scripts/emailOverviewForm.twig similarity index 99% rename from workers/email/scripts/emailOverviewForm.twig rename to workers/sender/scripts/emailOverviewForm.twig index 81de332d6..31a1a0359 100644 --- a/workers/email/scripts/emailOverviewForm.twig +++ b/workers/sender/scripts/emailOverviewForm.twig @@ -118,8 +118,8 @@

💌 Hawk Mailer

- - https://github.com/codex-team/hawk.workers/tree/master/workers/email + + https://github.com/codex-team/hawk.workers/tree/master/workers/sender diff --git a/workers/email/scripts/generateTemplatesNames.ts b/workers/sender/scripts/generateTemplatesNames.ts similarity index 90% rename from workers/email/scripts/generateTemplatesNames.ts rename to workers/sender/scripts/generateTemplatesNames.ts index ed8d0a046..70b7a19a9 100644 --- a/workers/email/scripts/generateTemplatesNames.ts +++ b/workers/sender/scripts/generateTemplatesNames.ts @@ -35,7 +35,7 @@ function formatName(name: string): string { /** * This folder contains available emails templates */ -const emailTemplatesDir = 'src/templates/emails'; +const emailTemplatesDir = 'src/providers/email/templates/emails'; /** * Path to email templates folder @@ -56,4 +56,4 @@ generatedEnum += 'enum Templates {\n'; generatedEnum += names.map((name: string) => ` ${formatName(name)} = '${name}',`).join('\n'); generatedEnum += '\n}\n\nexport default Templates;\n'; -fs.writeFileSync('../src/templates/names.ts', generatedEnum); +fs.writeFileSync('../src/providers/email/templates/names.ts', generatedEnum); diff --git a/workers/sender/src/channel-sender.ts b/workers/sender/src/channel-sender.ts new file mode 100644 index 000000000..75a274220 --- /dev/null +++ b/workers/sender/src/channel-sender.ts @@ -0,0 +1,792 @@ +import { + DecodedGroupedEvent, + ProjectDBScheme, + UserDBScheme, + GroupedEventDBScheme, + WorkspaceDBScheme, + ConfirmedMemberDBScheme, + PlanDBScheme +} from '@hawk.so/types'; +import { ObjectId } from 'mongodb'; +import { DatabaseController } from '../../../lib/db/controller'; +import { Worker } from '../../../lib/worker'; +import TimeMs from '../../../lib/utils/time'; + +import NotificationsProvider from './provider'; + +import { ChannelType } from 'hawk-worker-notifier/types/channel'; +import { + SenderWorkerEventTask, + SenderWorkerAssigneeTask, + SenderWorkerTask, + SenderWorkerBlockWorkspaceTask, + SenderWorkerPaymentFailedTask, + SenderWorkerPaymentSuccessTask, + SenderWorkerDaysLimitAlmostReachedTask, + SenderWorkerEventsLimitAlmostReachedTask, + SenderWorkerSignUpTask, + SenderWorkerPasswordResetTask, + SenderWorkerWorkspaceInviteTask, + SenderWorkerBlockedWorkspaceReminderTask +} from '../types/sender-task'; +import { decodeUnsafeFields } from '../../../lib/utils/unsafeFields'; +import { + Notification, + EventNotification, + SeveralEventsNotification, + PaymentFailedNotification, + AssigneeNotification, + SignUpNotification, + PasswordResetNotification, + PaymentSuccessNotification, + TemplateEventData, + WorkspaceInviteNotification +} from '../types/template-variables'; + +/** + * Worker of a single notification channel: consumes the `sender/` queue + * and delivers notifications through the channel provider. + * Created by the multi-channel SenderWorker (src/index.ts). + */ +export default class ChannelSenderWorker extends Worker { + /** + * Worker type: queue to consume + */ + public readonly type: string; + + /** + * Notifications provider + */ + protected provider: NotificationsProvider; + + /** + * Sender type. Used to get correct notifications endpoint from DB + */ + protected channelType: ChannelType; + + /** + * Database Controllers (shared between channels, connections are managed by SenderWorker) + */ + private eventsDb: DatabaseController; + private accountsDb: DatabaseController; + + /** + * @param channelType - channel to serve, defines the queue name + * @param provider - notifications provider of the channel + * @param eventsDb - events database controller + * @param accountsDb - accounts database controller + */ + constructor(channelType: ChannelType, provider: NotificationsProvider, eventsDb: DatabaseController, accountsDb: DatabaseController) { + super(); + + this.channelType = channelType; + this.provider = provider; + this.eventsDb = eventsDb; + this.accountsDb = accountsDb; + this.type = `sender/${channelType}`; + } + + /** + * Task handling function + * + * @param task - task to handle + */ + public async handle(task: T): Promise { + if (!this.channelType) { + throw new Error('channelType for Sender worker is not set'); + } + + if (!this.provider || typeof this.provider.send !== 'function') { + throw new Error('Notification Provider is not set or doesn\'t have `send` method'); + } + + if (!this.provider.logger) { + this.provider.setLogger(this.logger); + } + + switch (task.type) { + case 'assignee': + return this.handleAssigneeTask(task as SenderWorkerAssigneeTask); + case 'block-workspace': + return this.handleBlockWorkspaceTask(task as SenderWorkerBlockWorkspaceTask); + case 'blocked-workspace-reminder': + return this.handleBlockedWorkspaceReminderTask(task as SenderWorkerBlockedWorkspaceReminderTask); + case 'days-limit-almost-reached': + return this.handleDaysLimitAlmostReachedTask(task as SenderWorkerDaysLimitAlmostReachedTask); + case 'event': + return this.handleEventTask(task as SenderWorkerEventTask); + case 'events-limit-almost-reached': + return this.handleEventsLimitAlmostReachedTask(task as SenderWorkerEventsLimitAlmostReachedTask); + case 'payment-failed': + return this.handlePaymentFailedTask(task as SenderWorkerPaymentFailedTask); + case 'payment-success': + return this.handlePaymentSuccessTask(task as SenderWorkerPaymentSuccessTask); + case 'sign-up': + return this.handleSignUpTask(task as SenderWorkerSignUpTask); + case 'password-reset': + return this.handlePasswordResetTask(task as SenderWorkerPasswordResetTask); + case 'workspace-invite': + return this.handleWorkspaceInviteTask(task as SenderWorkerWorkspaceInviteTask); + } + } + + /** + * Handle event task + * + * @param task - task to handle + */ + private async handleEventTask(task: SenderWorkerEventTask): Promise { + const { projectId, ruleId, events } = task.payload; + + const project = await this.getProject(projectId); + + if (!project) { + this.logger.error(`Cannot send assignee notification: project not found. Payload: ${task}`); + + return; + } + + const rule = project.notifications.find((r) => r._id.toString() === ruleId); + + if (!rule) { + this.logger.error(`Cannot send assignee notification: notification rule not found. Payload: ${task}`); + + return; + } + + const channel = rule.channels[this.channelType]; + + if (!channel || !channel.endpoint) { + this.logger.error(`Cannot send assignee notification: channel not found. Payload: ${task}`); + + return; + } + + const eventsData = await Promise.all( + events.map( + async ({ key: groupHash, count, repetitionId }: { key: string; count: number; repetitionId?: string | null }): Promise => { + const [event, daysRepeated] = await this.getEventDataByGroupHash(projectId, groupHash); + + return { + event, + newCount: count, + daysRepeated, + repetitionId: repetitionId ?? null, + }; + } + ) + ); + + let notificationType: Notification['type'] = 'event'; + + if (eventsData.length > 1) { + notificationType = 'several-events'; + } + + this.logger.info(`Sending ${notificationType} notification to ${channel.endpoint}`); + + await this.provider.send(channel.endpoint, { + type: notificationType, + payload: { + host: process.env.GARAGE_URL, + hostOfStatic: process.env.API_STATIC_URL, + project, + events: eventsData, + period: channel.minPeriod, + notificationRuleId: rule._id.toString(), + }, + } as EventNotification | SeveralEventsNotification); + } + + /** + * Handle task when someone was assigned + * + * @param task - task to handle + */ + private async handleAssigneeTask(task: SenderWorkerAssigneeTask): Promise { + const { assigneeId, projectId, whoAssignedId, eventId, endpoint } = task.payload; + + const project = await this.getProject(projectId); + + if (!project) { + this.logger.error(`Cannot send assignee notification: project not found. Payload: ${task}`); + + return; + } + + const [event, daysRepeated] = await this.getEventData(projectId, eventId); + + if (!event) { + this.logger.error(`Cannot send assignee notification: event not found. Payload: ${task}`); + + return; + } + + const whoAssigned = await this.getUser(whoAssignedId); + + if (!whoAssigned) { + this.logger.error(`Cannot send assignee notification: user who assigned the person was not found. Payload: ${task}`); + + return; + } + + const assignee = await this.getUser(assigneeId); + + if (!assignee) { + this.logger.error(`Cannot send assignee notification: assignee not found. Payload: ${task}`); + + return; + } + + await this.provider.send(endpoint, { + type: 'assignee', + payload: { + host: process.env.GARAGE_URL, + hostOfStatic: process.env.API_STATIC_URL, + project, + event, + whoAssigned, + assignee, + daysRepeated, + }, + } as AssigneeNotification); + } + + /** + * Handle task when workspace blocked + * + * @param task - task to handle + */ + private async handleBlockWorkspaceTask(task: SenderWorkerBlockWorkspaceTask): Promise { + const { workspaceId } = task.payload; + + const workspace = await this.getWorkspace(workspaceId); + + if (!workspace) { + this.logger.error(`Cannot send block workspace notification: workspace not found. Payload: ${task}`); + + return; + } + + const admins = await this.getWorkspaceAdmins(workspaceId); + + if (!admins) { + this.logger.error(`Cannot send block workspace notification: workspace team not found. Payload: ${task}`); + + return; + } + + const adminIds = admins.map(admin => admin.userId.toString()); + const users = await this.getUsers(adminIds); + + await Promise.all(users.map(async user => { + const channel = user.notifications.channels[this.channelType]; + + if (channel.isEnabled) { + await this.provider.send(channel.endpoint, { + type: 'block-workspace', + payload: { + host: process.env.GARAGE_URL, + hostOfStatic: process.env.API_STATIC_URL, + workspace, + }, + }); + } + })); + } + + /** + * @param task + */ + private async handleBlockedWorkspaceReminderTask(task: SenderWorkerBlockedWorkspaceReminderTask): Promise { + const eventType = 'blocked-workspace-reminder'; + + /** + * Send message not often than once per day + */ + const throttleInterval = TimeMs.DAY; + + const { workspaceId, daysAfterBlock } = task.payload; + + const workspace = await this.getWorkspace(workspaceId); + + if (!workspace) { + this.logger.error(`Cannot send blocked workspace reminder notification: workspace not found. Payload: ${JSON.stringify(task)}`); + + return; + } + + const allowToSendNotification = this.needToSendNextNotification(workspace, eventType, throttleInterval); + + /** + * Do not send any notifications if we have already done in target throttle time + */ + if (!allowToSendNotification) { + return; + } + + const admins = await this.getWorkspaceAdmins(workspaceId); + + if (!admins) { + this.logger.error(`Cannot send blocked workspace reminder notification: workspace team not found. Payload: ${JSON.stringify(task)}`); + + return; + } + + const adminIds = admins.map(admin => admin.userId.toString()); + const users = await this.getUsers(adminIds); + + await Promise.all(users.map(async user => { + const channel = user.notifications.channels[this.channelType]; + + if (channel.isEnabled) { + await this.provider.send(channel.endpoint, { + type: 'blocked-workspace-reminder', + payload: { + host: process.env.GARAGE_URL, + hostOfStatic: process.env.API_STATIC_URL, + workspace, + daysAfterBlock, + }, + }); + } + })); + + await this.updateLastNotificationDate(workspace, eventType); + } + + /** + * Handle task when days limit is almost reached + * + * @param task - task to handle + */ + private async handleDaysLimitAlmostReachedTask(task: SenderWorkerDaysLimitAlmostReachedTask): Promise { + const eventType = 'days-limit-almost-reached'; + + /** + * Send message not often than once per day + */ + const throttleInterval = TimeMs.DAY; + + const { workspaceId, daysLeft } = task.payload; + + const workspace = await this.getWorkspace(workspaceId); + + if (!workspace) { + this.logger.error(`Cannot send days limit reached notification: workspace not found. Payload: ${task}`); + + return; + } + + const allowToSendNotification = this.needToSendNextNotification(workspace, eventType, throttleInterval); + + /** + * Do not send any notifications if we have already done in target throttle time + */ + if (!allowToSendNotification) { + return; + } + + const admins = await this.getWorkspaceAdmins(workspaceId); + + if (!admins) { + this.logger.error(`Cannot send days limit reached notification: workspace team not found. Payload: ${task}`); + + return; + } + + const adminIds = admins.map(admin => admin.userId.toString()); + const users = await this.getUsers(adminIds); + + await Promise.all(users.map(async user => { + const channel = user.notifications.channels[this.channelType]; + + if (channel.isEnabled) { + /** + * Send message + */ + await this.provider.send(channel.endpoint, { + type: eventType, + payload: { + host: process.env.GARAGE_URL, + hostOfStatic: process.env.API_STATIC_URL, + workspace, + daysLeft, + }, + }); + } + })); + + await this.updateLastNotificationDate(workspace, eventType); + } + + /** + * Handle task when events limit is almost reached + * + * @param task - task to handle + */ + private async handleEventsLimitAlmostReachedTask(task: SenderWorkerEventsLimitAlmostReachedTask): Promise { + const eventType = 'events-limit-almost-reached'; + + /** + * Send message not often than once per day + */ + const throttleInterval = TimeMs.DAY; + + const { workspaceId, eventsCount, eventsLimit } = task.payload; + + const workspace = await this.getWorkspace(workspaceId); + + if (!workspace) { + this.logger.error(`Cannot send events limit reached notification: workspace not found. Payload: ${task}`); + + return; + } + + const allowToSendNotification = this.needToSendNextNotification(workspace, eventType, throttleInterval); + + /** + * Do not send any notifications if we have already done in target throttle time + */ + if (!allowToSendNotification) { + return; + } + + const admins = await this.getWorkspaceAdmins(workspaceId); + + if (!admins) { + this.logger.error(`Cannot send events limit reached notification: workspace team not found. Payload: ${task}`); + + return; + } + + const adminIds = admins.map(admin => admin.userId.toString()); + const users = await this.getUsers(adminIds); + + await Promise.all(users.map(async user => { + const channel = user.notifications.channels[this.channelType]; + + if (channel.isEnabled) { + await this.provider.send(channel.endpoint, { + type: eventType, + payload: { + host: process.env.GARAGE_URL, + hostOfStatic: process.env.API_STATIC_URL, + workspace, + eventsCount, + eventsLimit, + }, + }); + } + })); + + await this.updateLastNotificationDate(workspace, eventType); + } + + /** + * Handle task when user payment failed + * + * @param task - task to handle + */ + private async handlePaymentFailedTask(task: SenderWorkerPaymentFailedTask): Promise { + const { workspaceId, reason, endpoint } = task.payload; + + const workspace = await this.getWorkspace(workspaceId); + + if (!workspace) { + this.logger.error(`Cannot send payment failed notification: workspace not found. Payload: ${task}`); + + return; + } + + await this.provider.send(endpoint, { + type: 'payment-failed', + payload: { + host: process.env.GARAGE_URL, + hostOfStatic: process.env.API_STATIC_URL, + workspace, + reason, + }, + } as PaymentFailedNotification); + } + + /** + * Handle task when user has successfully paid + * + * @param task - task to handle + */ + private async handlePaymentSuccessTask(task: SenderWorkerPaymentSuccessTask): Promise { + const { workspaceId, tariffPlanId, endpoint } = task.payload; + + const workspace = await this.getWorkspace(workspaceId); + + if (!workspace) { + this.logger.error(`Cannot send payment success notification: workspace not found. Payload: ${task}`); + + return; + } + + const plan = await this.getPlan(tariffPlanId); + + if (!plan) { + this.logger.error(`Cannot send payment success notification: plan not found. Payload: ${task}`); + + return; + } + + await this.provider.send(endpoint, { + type: 'payment-success', + payload: { + host: process.env.GARAGE_URL, + hostOfStatic: process.env.API_STATIC_URL, + workspace, + plan, + }, + } as PaymentSuccessNotification); + } + + /** + * Handle task when user recovers his password + * + * @param task - task to handle + */ + private async handlePasswordResetTask(task: SenderWorkerPasswordResetTask): Promise { + const { newPassword, endpoint } = task.payload; + + await this.provider.send(endpoint, { + type: 'password-reset', + payload: { + host: process.env.GARAGE_URL, + hostOfStatic: process.env.API_STATIC_URL, + password: newPassword, + }, + } as PasswordResetNotification); + } + + /** + * Handle task when user recovers his password + * + * @param task - task to handle + */ + private async handleWorkspaceInviteTask(task: SenderWorkerWorkspaceInviteTask): Promise { + const { workspaceName, inviteLink, endpoint } = task.payload; + + await this.provider.send(endpoint, { + type: 'workspace-invite', + payload: { + host: process.env.GARAGE_URL, + hostOfStatic: process.env.API_STATIC_URL, + workspaceName, + inviteLink, + }, + } as WorkspaceInviteNotification); + } + + /** + * Handle task when user has successfully registered + * + * @param task - task to handle + */ + private async handleSignUpTask(task: SenderWorkerSignUpTask): Promise { + const { password, endpoint } = task.payload; + + await this.provider.send(endpoint, { + type: 'sign-up', + payload: { + host: process.env.GARAGE_URL, + hostOfStatic: process.env.API_STATIC_URL, + password, + email: endpoint, + }, + } as SignUpNotification); + } + + /** + * Get event data for email + * + * @param {string} projectId - project events are related to + * @param {string} groupHash - event group hash + */ + private async getEventDataByGroupHash( + projectId: string, + groupHash: string + ): Promise<[DecodedGroupedEvent, number]> { + const connection = await this.eventsDb.getConnection(); + + const event = await connection.collection(`events:${projectId}`).findOne({ groupHash }); + + decodeUnsafeFields(event); + + const daysRepeated = await connection.collection(`dailyEvents:${projectId}`).countDocuments({ + groupHash, + }); + + return [event, daysRepeated]; + } + + /** + * Get event data by projectId and eventId + * + * @param projectId - project id of the event + * @param eventId - id of the event + */ + private async getEventData(projectId: string, eventId: string): Promise<[GroupedEventDBScheme, number]> { + const connection = await this.eventsDb.getConnection(); + + const event = await connection.collection(`events:${projectId}`).findOne({ + _id: new ObjectId(eventId), + }); + const daysRepeated = await connection.collection(`dailyEvents:${projectId}`).countDocuments({ + groupHash: event.groupHash, + }); + + return [event, daysRepeated]; + } + + /** + * Get project info + * + * @param projectId - project id + */ + private async getProject(projectId: string): Promise { + const connection = await this.accountsDb.getConnection(); + + return connection.collection('projects').findOne({ _id: new ObjectId(projectId) }); + } + + /** + * Gets workspace info from database + * + * @param workspaceId - workspace id for search + */ + private async getWorkspace(workspaceId: string): Promise { + const connection = await this.accountsDb.getConnection(); + + return connection.collection('workspaces').findOne({ _id: new ObjectId(workspaceId) }); + } + + /** + * Gets confirmed admins by workspace id + * + * @param workspaceId - workspace id for search + */ + private async getWorkspaceAdmins(workspaceId: string): Promise { + const connection = await this.accountsDb.getConnection(); + + return connection.collection(`team:${workspaceId}`).find({ + userId: { $exists: true }, + isAdmin: true, + }) + .toArray(); + } + + /** + * Get user data + * + * @param userId - user id + */ + private async getUser(userId: string): Promise { + const connection = await this.accountsDb.getConnection(); + + return connection.collection('users').findOne({ _id: new ObjectId(userId) }); + } + + /** + * Gets array of users from database + * + * @param userIds - user ids for search + */ + private async getUsers(userIds: string[]): Promise { + const connection = await this.accountsDb.getConnection(); + + return connection.collection('users').find({ + _id: { + $in: userIds.map(userId => new ObjectId(userId)), + }, + }) + .toArray(); + } + + /** + * Get tariff plan + * + * @param planId - workspace plan id + */ + private async getPlan(planId: string): Promise { + const connection = await this.accountsDb.getConnection(); + + return connection.collection('plans').findOne({ _id: new ObjectId(planId) }); + } + + /** + * @private + * @param {WorkspaceDBScheme} workspace - workspace object + * @param {string} type - event type + * @param {number} throttleTime - number of ms as interval for rejecting sending + * + * @returns {boolean} - should we send a message or not + */ + private needToSendNextNotification(workspace: WorkspaceDBScheme, type: string, throttleTime: number): boolean { + if (!throttleTime) { + return true; + } + + /** + * Throw an error if workspace is missing + */ + if (!workspace) { + throw Error('Workspace is missing'); + } + + /** + * If lastNotificationDate is missing then do not block anything + */ + if (!workspace.lastNotificationDate) { + return true; + } + + const lastNotificationDateForType = workspace.lastNotificationDate[type]; + + /** + * If date is missing then allow action + */ + if (!lastNotificationDateForType) { + return true; + } + + /** + * Calculate current interval from last notification + */ + const currentInterval = Date.now() - lastNotificationDateForType.getTime(); + + /** + * Return true if we can send a new notification message + * Otherwise it is not time yet + */ + return currentInterval >= throttleTime; + } + + /** + * @param {WorkspaceDBScheme} workspace - workspace object + * @param {string} type - event type + * @param {number} date - date to be set + */ + private async updateLastNotificationDate(workspace: WorkspaceDBScheme, type: string, date = new Date()): Promise { + /** + * Throw an error if workspace is missing + */ + if (!workspace) { + throw Error('Workspace is missing'); + } + + const connection = await this.accountsDb.getConnection(); + + await connection.collection('workspaces').updateOne({ + _id: workspace._id, + }, { + $set: { + [`lastNotificationDate.${type}`]: date, + }, + }); + } +} diff --git a/workers/sender/src/channels.ts b/workers/sender/src/channels.ts new file mode 100644 index 000000000..e1b4da73e --- /dev/null +++ b/workers/sender/src/channels.ts @@ -0,0 +1,20 @@ +import { ChannelType } from 'hawk-worker-notifier/types/channel'; +import NotificationsProvider from './provider'; +import EmailProvider from './providers/email/provider'; +import TelegramProvider from './providers/telegram/provider'; +import SlackProvider from './providers/slack/provider'; +import WebhookProvider from './providers/webhook/provider'; +import LoopProvider from './providers/loop/provider'; + +/** + * Registry of notification channels and their providers + */ +const channelProviders: Record NotificationsProvider> = { + [ChannelType.Email]: EmailProvider, + [ChannelType.Telegram]: TelegramProvider, + [ChannelType.Slack]: SlackProvider, + [ChannelType.Webhook]: WebhookProvider, + [ChannelType.Loop]: LoopProvider, +}; + +export default channelProviders; diff --git a/workers/sender/src/index.ts b/workers/sender/src/index.ts index a52b05c58..c6b901622 100644 --- a/workers/sender/src/index.ts +++ b/workers/sender/src/index.ts @@ -1,71 +1,42 @@ -import { - DecodedGroupedEvent, - ProjectDBScheme, - UserDBScheme, - GroupedEventDBScheme, - WorkspaceDBScheme, - ConfirmedMemberDBScheme, - PlanDBScheme -} from '@hawk.so/types'; -import { ObjectId } from 'mongodb'; +import { ChannelType } from 'hawk-worker-notifier/types/channel'; import { DatabaseController } from '../../../lib/db/controller'; -import { Worker } from '../../../lib/worker'; +import createLogger from '../../../lib/logger'; import * as pkg from '../package.json'; import './env'; -import TimeMs from '../../../lib/utils/time'; - -import { PasswordResetNotification, PaymentSuccessNotification, TemplateEventData, WorkspaceInviteNotification } from '../types/template-variables/'; -import NotificationsProvider from './provider'; - -import { ChannelType } from 'hawk-worker-notifier/types/channel'; -import { - SenderWorkerEventTask, - SenderWorkerAssigneeTask, - SenderWorkerTask, - SenderWorkerBlockWorkspaceTask, - SenderWorkerPaymentFailedTask, - SenderWorkerPaymentSuccessTask, - SenderWorkerDaysLimitAlmostReachedTask, - SenderWorkerEventsLimitAlmostReachedTask, - SenderWorkerSignUpTask, - SenderWorkerPasswordResetTask, - SenderWorkerWorkspaceInviteTask, - SenderWorkerBlockedWorkspaceReminderTask -} from '../types/sender-task'; -import { decodeUnsafeFields } from '../../../lib/utils/unsafeFields'; -import { Notification, EventNotification, SeveralEventsNotification, PaymentFailedNotification, AssigneeNotification, SignUpNotification } from '../types/template-variables'; +import ChannelSenderWorker from './channel-sender'; +import channelProviders from './channels'; /** - * Worker to send email notifications + * Multi-channel sender worker: starts a ChannelSenderWorker per enabled channel. + * Channels are set by SENDER_CHANNELS (comma-separated, all by default), + * each consumes its own `sender/` queue, MongoDB connections are shared. */ -export default abstract class SenderWorker extends Worker { +export default class SenderWorker { /** - * Worker type + * Worker type. Used for metrics and logs */ public readonly type: string = pkg.workerType; /** - * Database Controllers + * Logger module */ - private eventsDb: DatabaseController = new DatabaseController(process.env.MONGO_EVENTS_DATABASE_URI); - private accountsDb: DatabaseController = new DatabaseController(process.env.MONGO_ACCOUNTS_DATABASE_URI); + private logger = createLogger(); /** - * Notifications provider + * Database Controllers shared between channel workers */ - protected abstract provider: NotificationsProvider; + private eventsDb = new DatabaseController(process.env.MONGO_EVENTS_DATABASE_URI); + private accountsDb = new DatabaseController(process.env.MONGO_ACCOUNTS_DATABASE_URI); /** - * Sender type. Used to get correct notifications endpoint from DB + * Workers of enabled channels */ - protected abstract channelType: ChannelType; + private channelWorkers: ChannelSenderWorker[]; /** - * Constructor uses to check required ENV params + * Checks required ENV params and creates channel workers */ constructor() { - super(); - if (!process.env.GARAGE_URL) { throw Error('process.env.GARAGE_URL does not specified. Check workers/sender/.env'); } @@ -73,727 +44,59 @@ export default abstract class SenderWorker extends Worker { if (!process.env.API_STATIC_URL) { throw Error('process.env.API_STATIC_URL does not specified. Check workers/sender/.env'); } + + this.channelWorkers = this.getEnabledChannels().map((channel) => { + return new ChannelSenderWorker(channel, new channelProviders[channel](), this.eventsDb, this.accountsDb); + }); } /** - * Start consuming messages + * Connect to databases and start consuming channel queues */ public async start(): Promise { await this.eventsDb.connect(); await this.accountsDb.connect(); - await super.start(); - } - - /** - * Finish everything - */ - public async finish(): Promise { - await super.finish(); - await this.eventsDb.close(); - await this.accountsDb.close(); - } - - /** - * Task handling function - * - * @param task - task to handle - */ - public async handle(task: T): Promise { - if (!this.channelType) { - throw new Error('channelType for Sender worker is not set'); - } - - if (!this.provider || typeof this.provider.send !== 'function') { - throw new Error('Notification Provider is not set or doesn\'t have `send` method'); - } - - if (!this.provider.logger) { - this.provider.setLogger(this.logger); - } - - switch (task.type) { - case 'assignee': - return this.handleAssigneeTask(task as SenderWorkerAssigneeTask); - case 'block-workspace': - return this.handleBlockWorkspaceTask(task as SenderWorkerBlockWorkspaceTask); - case 'blocked-workspace-reminder': - return this.handleBlockedWorkspaceReminderTask(task as SenderWorkerBlockedWorkspaceReminderTask); - case 'days-limit-almost-reached': - return this.handleDaysLimitAlmostReachedTask(task as SenderWorkerDaysLimitAlmostReachedTask); - case 'event': - return this.handleEventTask(task as SenderWorkerEventTask); - case 'events-limit-almost-reached': - return this.handleEventsLimitAlmostReachedTask(task as SenderWorkerEventsLimitAlmostReachedTask); - case 'payment-failed': - return this.handlePaymentFailedTask(task as SenderWorkerPaymentFailedTask); - case 'payment-success': - return this.handlePaymentSuccessTask(task as SenderWorkerPaymentSuccessTask); - case 'sign-up': - return this.handleSignUpTask(task as SenderWorkerSignUpTask); - case 'password-reset': - return this.handlePasswordResetTask(task as SenderWorkerPasswordResetTask); - case 'workspace-invite': - return this.handleWorkspaceInviteTask(task as SenderWorkerWorkspaceInviteTask); - } - } - - /** - * Handle event task - * - * @param task - task to handle - */ - private async handleEventTask(task: SenderWorkerEventTask): Promise { - const { projectId, ruleId, events } = task.payload; - - const project = await this.getProject(projectId); - - if (!project) { - this.logger.error(`Cannot send assignee notification: project not found. Payload: ${task}`); - - return; - } - - const rule = project.notifications.find((r) => r._id.toString() === ruleId); - - if (!rule) { - this.logger.error(`Cannot send assignee notification: notification rule not found. Payload: ${task}`); - - return; - } - - const channel = rule.channels[this.channelType]; - - if (!channel || !channel.endpoint) { - this.logger.error(`Cannot send assignee notification: channel not found. Payload: ${task}`); - - return; - } - - const eventsData = await Promise.all( - events.map( - async ({ key: groupHash, count, repetitionId }: { key: string; count: number; repetitionId?: string | null }): Promise => { - const [event, daysRepeated] = await this.getEventDataByGroupHash(projectId, groupHash); - - return { - event, - newCount: count, - daysRepeated, - repetitionId: repetitionId ?? null, - }; - } - ) - ); - - let notificationType: Notification['type'] = 'event'; - - if (eventsData.length > 1) { - notificationType = 'several-events'; - } - - this.logger.info(`Sending ${notificationType} notification to ${channel.endpoint}`); - - await this.provider.send(channel.endpoint, { - type: notificationType, - payload: { - host: process.env.GARAGE_URL, - hostOfStatic: process.env.API_STATIC_URL, - project, - events: eventsData, - period: channel.minPeriod, - notificationRuleId: rule._id.toString(), - }, - } as EventNotification | SeveralEventsNotification); - } - - /** - * Handle task when someone was assigned - * - * @param task - task to handle - */ - private async handleAssigneeTask(task: SenderWorkerAssigneeTask): Promise { - const { assigneeId, projectId, whoAssignedId, eventId, endpoint } = task.payload; - - const project = await this.getProject(projectId); - - if (!project) { - this.logger.error(`Cannot send assignee notification: project not found. Payload: ${task}`); - - return; - } - - const [event, daysRepeated] = await this.getEventData(projectId, eventId); - - if (!event) { - this.logger.error(`Cannot send assignee notification: event not found. Payload: ${task}`); - - return; - } - - const whoAssigned = await this.getUser(whoAssignedId); - - if (!whoAssigned) { - this.logger.error(`Cannot send assignee notification: user who assigned the person was not found. Payload: ${task}`); - - return; - } - - const assignee = await this.getUser(assigneeId); - - if (!assignee) { - this.logger.error(`Cannot send assignee notification: assignee not found. Payload: ${task}`); - - return; - } - - await this.provider.send(endpoint, { - type: 'assignee', - payload: { - host: process.env.GARAGE_URL, - hostOfStatic: process.env.API_STATIC_URL, - project, - event, - whoAssigned, - assignee, - daysRepeated, - }, - } as AssigneeNotification); - } - - /** - * Handle task when workspace blocked - * - * @param task - task to handle - */ - private async handleBlockWorkspaceTask(task: SenderWorkerBlockWorkspaceTask): Promise { - const { workspaceId } = task.payload; - - const workspace = await this.getWorkspace(workspaceId); - - if (!workspace) { - this.logger.error(`Cannot send block workspace notification: workspace not found. Payload: ${task}`); - - return; - } - - const admins = await this.getWorkspaceAdmins(workspaceId); - - if (!admins) { - this.logger.error(`Cannot send block workspace notification: workspace team not found. Payload: ${task}`); - - return; - } - - const adminIds = admins.map(admin => admin.userId.toString()); - const users = await this.getUsers(adminIds); - - await Promise.all(users.map(async user => { - const channel = user.notifications.channels[this.channelType]; - - if (channel.isEnabled) { - await this.provider.send(channel.endpoint, { - type: 'block-workspace', - payload: { - host: process.env.GARAGE_URL, - hostOfStatic: process.env.API_STATIC_URL, - workspace, - }, - }); - } - })); - } - - /** - * @param task - */ - private async handleBlockedWorkspaceReminderTask(task: SenderWorkerBlockedWorkspaceReminderTask): Promise { - const eventType = 'blocked-workspace-reminder'; - - /** - * Send message not often than once per day - */ - const throttleInterval = TimeMs.DAY; - - const { workspaceId, daysAfterBlock } = task.payload; - - const workspace = await this.getWorkspace(workspaceId); - - if (!workspace) { - this.logger.error(`Cannot send blocked workspace reminder notification: workspace not found. Payload: ${JSON.stringify(task)}`); - - return; - } - - const allowToSendNotification = this.needToSendNextNotification(workspace, eventType, throttleInterval); - - /** - * Do not send any notifications if we have already done in target throttle time - */ - if (!allowToSendNotification) { - return; - } - - const admins = await this.getWorkspaceAdmins(workspaceId); - - if (!admins) { - this.logger.error(`Cannot send blocked workspace reminder notification: workspace team not found. Payload: ${JSON.stringify(task)}`); - - return; - } - - const adminIds = admins.map(admin => admin.userId.toString()); - const users = await this.getUsers(adminIds); - - await Promise.all(users.map(async user => { - const channel = user.notifications.channels[this.channelType]; - - if (channel.isEnabled) { - await this.provider.send(channel.endpoint, { - type: 'blocked-workspace-reminder', - payload: { - host: process.env.GARAGE_URL, - hostOfStatic: process.env.API_STATIC_URL, - workspace, - daysAfterBlock, - }, - }); - } - })); - - await this.updateLastNotificationDate(workspace, eventType); - } - - /** - * Handle task when days limit is almost reached - * - * @param task - task to handle - */ - private async handleDaysLimitAlmostReachedTask(task: SenderWorkerDaysLimitAlmostReachedTask): Promise { - const eventType = 'days-limit-almost-reached'; - - /** - * Send message not often than once per day - */ - const throttleInterval = TimeMs.DAY; - - const { workspaceId, daysLeft } = task.payload; - - const workspace = await this.getWorkspace(workspaceId); - - if (!workspace) { - this.logger.error(`Cannot send days limit reached notification: workspace not found. Payload: ${task}`); - - return; - } - - const allowToSendNotification = this.needToSendNextNotification(workspace, eventType, throttleInterval); - - /** - * Do not send any notifications if we have already done in target throttle time - */ - if (!allowToSendNotification) { - return; - } - const admins = await this.getWorkspaceAdmins(workspaceId); + await Promise.all(this.channelWorkers.map((worker) => worker.start())); - if (!admins) { - this.logger.error(`Cannot send days limit reached notification: workspace team not found. Payload: ${task}`); - - return; - } - - const adminIds = admins.map(admin => admin.userId.toString()); - const users = await this.getUsers(adminIds); - - await Promise.all(users.map(async user => { - const channel = user.notifications.channels[this.channelType]; - - if (channel.isEnabled) { - /** - * Send message - */ - await this.provider.send(channel.endpoint, { - type: eventType, - payload: { - host: process.env.GARAGE_URL, - hostOfStatic: process.env.API_STATIC_URL, - workspace, - daysLeft, - }, - }); - } - })); - - await this.updateLastNotificationDate(workspace, eventType); - } - - /** - * Handle task when events limit is almost reached - * - * @param task - task to handle - */ - private async handleEventsLimitAlmostReachedTask(task: SenderWorkerEventsLimitAlmostReachedTask): Promise { - const eventType = 'events-limit-almost-reached'; - - /** - * Send message not often than once per day - */ - const throttleInterval = TimeMs.DAY; - - const { workspaceId, eventsCount, eventsLimit } = task.payload; - - const workspace = await this.getWorkspace(workspaceId); - - if (!workspace) { - this.logger.error(`Cannot send events limit reached notification: workspace not found. Payload: ${task}`); - - return; - } - - const allowToSendNotification = this.needToSendNextNotification(workspace, eventType, throttleInterval); - - /** - * Do not send any notifications if we have already done in target throttle time - */ - if (!allowToSendNotification) { - return; - } - - const admins = await this.getWorkspaceAdmins(workspaceId); - - if (!admins) { - this.logger.error(`Cannot send events limit reached notification: workspace team not found. Payload: ${task}`); - - return; - } - - const adminIds = admins.map(admin => admin.userId.toString()); - const users = await this.getUsers(adminIds); - - await Promise.all(users.map(async user => { - const channel = user.notifications.channels[this.channelType]; - - if (channel.isEnabled) { - await this.provider.send(channel.endpoint, { - type: eventType, - payload: { - host: process.env.GARAGE_URL, - hostOfStatic: process.env.API_STATIC_URL, - workspace, - eventsCount, - eventsLimit, - }, - }); - } - })); - - await this.updateLastNotificationDate(workspace, eventType); - } - - /** - * Handle task when user payment failed - * - * @param task - task to handle - */ - private async handlePaymentFailedTask(task: SenderWorkerPaymentFailedTask): Promise { - const { workspaceId, reason, endpoint } = task.payload; - - const workspace = await this.getWorkspace(workspaceId); - - if (!workspace) { - this.logger.error(`Cannot send payment failed notification: workspace not found. Payload: ${task}`); - - return; - } - - await this.provider.send(endpoint, { - type: 'payment-failed', - payload: { - host: process.env.GARAGE_URL, - hostOfStatic: process.env.API_STATIC_URL, - workspace, - reason, - }, - } as PaymentFailedNotification); - } - - /** - * Handle task when user has successfully paid - * - * @param task - task to handle - */ - private async handlePaymentSuccessTask(task: SenderWorkerPaymentSuccessTask): Promise { - const { workspaceId, tariffPlanId, endpoint } = task.payload; - - const workspace = await this.getWorkspace(workspaceId); - - if (!workspace) { - this.logger.error(`Cannot send payment success notification: workspace not found. Payload: ${task}`); - - return; - } - - const plan = await this.getPlan(tariffPlanId); - - if (!plan) { - this.logger.error(`Cannot send payment success notification: plan not found. Payload: ${task}`); - - return; - } - - await this.provider.send(endpoint, { - type: 'payment-success', - payload: { - host: process.env.GARAGE_URL, - hostOfStatic: process.env.API_STATIC_URL, - workspace, - plan, - }, - } as PaymentSuccessNotification); - } - - /** - * Handle task when user recovers his password - * - * @param task - task to handle - */ - private async handlePasswordResetTask(task: SenderWorkerPasswordResetTask): Promise { - const { newPassword, endpoint } = task.payload; - - await this.provider.send(endpoint, { - type: 'password-reset', - payload: { - host: process.env.GARAGE_URL, - hostOfStatic: process.env.API_STATIC_URL, - password: newPassword, - }, - } as PasswordResetNotification); - } - - /** - * Handle task when user recovers his password - * - * @param task - task to handle - */ - private async handleWorkspaceInviteTask(task: SenderWorkerWorkspaceInviteTask): Promise { - const { workspaceName, inviteLink, endpoint } = task.payload; - - await this.provider.send(endpoint, { - type: 'workspace-invite', - payload: { - host: process.env.GARAGE_URL, - hostOfStatic: process.env.API_STATIC_URL, - workspaceName, - inviteLink, - }, - } as WorkspaceInviteNotification); + this.logger.info(`Sender started with channels: ${this.channelWorkers.map((worker) => worker.type).join(', ')}`); } /** - * Handle task when user has successfully registered - * - * @param task - task to handle + * Finish channel workers and close database connections */ - private async handleSignUpTask(task: SenderWorkerSignUpTask): Promise { - const { password, endpoint } = task.payload; - - await this.provider.send(endpoint, { - type: 'sign-up', - payload: { - host: process.env.GARAGE_URL, - hostOfStatic: process.env.API_STATIC_URL, - password, - email: endpoint, - }, - } as SignUpNotification); - } - - /** - * Get event data for email - * - * @param {string} projectId - project events are related to - * @param {string} groupHash - event group hash - */ - private async getEventDataByGroupHash( - projectId: string, - groupHash: string - ): Promise<[DecodedGroupedEvent, number]> { - const connection = await this.eventsDb.getConnection(); - - const event = await connection.collection(`events:${projectId}`).findOne({ groupHash }); - - decodeUnsafeFields(event); - - const daysRepeated = await connection.collection(`dailyEvents:${projectId}`).countDocuments({ - groupHash, - }); - - return [event, daysRepeated]; - } - - /** - * Get event data by projectId and eventId - * - * @param projectId - project id of the event - * @param eventId - id of the event - */ - private async getEventData(projectId: string, eventId: string): Promise<[GroupedEventDBScheme, number]> { - const connection = await this.eventsDb.getConnection(); - - const event = await connection.collection(`events:${projectId}`).findOne({ - _id: new ObjectId(eventId), - }); - const daysRepeated = await connection.collection(`dailyEvents:${projectId}`).countDocuments({ - groupHash: event.groupHash, - }); - - return [event, daysRepeated]; - } - - /** - * Get project info - * - * @param projectId - project id - */ - private async getProject(projectId: string): Promise { - const connection = await this.accountsDb.getConnection(); - - return connection.collection('projects').findOne({ _id: new ObjectId(projectId) }); - } - - /** - * Gets workspace info from database - * - * @param workspaceId - workspace id for search - */ - private async getWorkspace(workspaceId: string): Promise { - const connection = await this.accountsDb.getConnection(); - - return connection.collection('workspaces').findOne({ _id: new ObjectId(workspaceId) }); - } - - /** - * Gets confirmed admins by workspace id - * - * @param workspaceId - workspace id for search - */ - private async getWorkspaceAdmins(workspaceId: string): Promise { - const connection = await this.accountsDb.getConnection(); - - return connection.collection(`team:${workspaceId}`).find({ - userId: { $exists: true }, - isAdmin: true, - }) - .toArray(); - } - - /** - * Get user data - * - * @param userId - user id - */ - private async getUser(userId: string): Promise { - const connection = await this.accountsDb.getConnection(); - - return connection.collection('users').findOne({ _id: new ObjectId(userId) }); - } - - /** - * Gets array of users from database - * - * @param userIds - user ids for search - */ - private async getUsers(userIds: string[]): Promise { - const connection = await this.accountsDb.getConnection(); - - return connection.collection('users').find({ - _id: { - $in: userIds.map(userId => new ObjectId(userId)), - }, - }) - .toArray(); - } - - /** - * Get tariff plan - * - * @param planId - workspace plan id - */ - private async getPlan(planId: string): Promise { - const connection = await this.accountsDb.getConnection(); + public async finish(): Promise { + await Promise.all(this.channelWorkers.map((worker) => worker.finish())); - return connection.collection('plans').findOne({ _id: new ObjectId(planId) }); + await this.eventsDb.close(); + await this.accountsDb.close(); } /** - * @private - * @param {WorkspaceDBScheme} workspace - workspace object - * @param {string} type - event type - * @param {number} throttleTime - number of ms as interval for rejecting sending - * - * @returns {boolean} - should we send a message or not + * Parse SENDER_CHANNELS env variable. All known channels are enabled when it is not set */ - private needToSendNextNotification(workspace: WorkspaceDBScheme, type: string, throttleTime: number): boolean { - if (!throttleTime) { - return true; - } + private getEnabledChannels(): ChannelType[] { + const knownChannels = Object.keys(channelProviders) as ChannelType[]; - /** - * Throw an error if workspace is missing - */ - if (!workspace) { - throw Error('Workspace is missing'); + if (!process.env.SENDER_CHANNELS) { + return knownChannels; } - /** - * If lastNotificationDate is missing then do not block anything - */ - if (!workspace.lastNotificationDate) { - return true; - } + const channels = process.env.SENDER_CHANNELS + .split(',') + .map((channel) => channel.trim()) + .filter(Boolean) as ChannelType[]; - const lastNotificationDateForType = workspace.lastNotificationDate[type]; + const unknownChannels = channels.filter((channel) => !knownChannels.includes(channel)); - /** - * If date is missing then allow action - */ - if (!lastNotificationDateForType) { - return true; + if (unknownChannels.length > 0) { + throw Error(`Unknown channels in SENDER_CHANNELS: ${unknownChannels.join(', ')}. Available: ${knownChannels.join(', ')}`); } - /** - * Calculate current interval from last notification - */ - const currentInterval = Date.now() - lastNotificationDateForType.getTime(); - - /** - * Return true if we can send a new notification message - * Otherwise it is not time yet - */ - return currentInterval >= throttleTime; - } - - /** - * @param {WorkspaceDBScheme} workspace - workspace object - * @param {string} type - event type - * @param {number} date - date to be set - */ - private async updateLastNotificationDate(workspace: WorkspaceDBScheme, type: string, date = new Date()): Promise { - /** - * Throw an error if workspace is missing - */ - if (!workspace) { - throw Error('Workspace is missing'); + if (channels.length === 0) { + throw Error('SENDER_CHANNELS is set but contains no channels'); } - const connection = await this.accountsDb.getConnection(); - - await connection.collection('workspaces').updateOne({ - _id: workspace._id, - }, { - $set: { - [`lastNotificationDate.${type}`]: date, - }, - }); + return channels; } } diff --git a/workers/email/src/provider.ts b/workers/sender/src/providers/email/provider.ts similarity index 98% rename from workers/email/src/provider.ts rename to workers/sender/src/providers/email/provider.ts index 9ffaba12c..16e2de1b5 100644 --- a/workers/email/src/provider.ts +++ b/workers/sender/src/providers/email/provider.ts @@ -3,7 +3,7 @@ import * as Twig from 'twig'; import { Notification, TemplateVariables } from 'hawk-worker-sender/types/template-variables'; import templates, { Template } from './templates'; import NotificationsProvider from 'hawk-worker-sender/src/provider'; -import * as utils from '../../../lib/utils'; +import * as utils from '../../../../../lib/utils'; import Templates from './templates/names'; diff --git a/workers/email/src/templates/components/backtrace.twig b/workers/sender/src/providers/email/templates/components/backtrace.twig similarity index 100% rename from workers/email/src/templates/components/backtrace.twig rename to workers/sender/src/providers/email/templates/components/backtrace.twig diff --git a/workers/email/src/templates/components/button.twig b/workers/sender/src/providers/email/templates/components/button.twig similarity index 100% rename from workers/email/src/templates/components/button.twig rename to workers/sender/src/providers/email/templates/components/button.twig diff --git a/workers/email/src/templates/components/event-info.twig b/workers/sender/src/providers/email/templates/components/event-info.twig similarity index 100% rename from workers/email/src/templates/components/event-info.twig rename to workers/sender/src/providers/email/templates/components/event-info.twig diff --git a/workers/email/src/templates/components/event.twig b/workers/sender/src/providers/email/templates/components/event.twig similarity index 100% rename from workers/email/src/templates/components/event.twig rename to workers/sender/src/providers/email/templates/components/event.twig diff --git a/workers/email/src/templates/components/filename.twig b/workers/sender/src/providers/email/templates/components/filename.twig similarity index 100% rename from workers/email/src/templates/components/filename.twig rename to workers/sender/src/providers/email/templates/components/filename.twig diff --git a/workers/email/src/templates/components/layout.twig b/workers/sender/src/providers/email/templates/components/layout.twig similarity index 100% rename from workers/email/src/templates/components/layout.twig rename to workers/sender/src/providers/email/templates/components/layout.twig diff --git a/workers/email/src/templates/components/project.twig b/workers/sender/src/providers/email/templates/components/project.twig similarity index 100% rename from workers/email/src/templates/components/project.twig rename to workers/sender/src/providers/email/templates/components/project.twig diff --git a/workers/email/src/templates/components/workspace.twig b/workers/sender/src/providers/email/templates/components/workspace.twig similarity index 100% rename from workers/email/src/templates/components/workspace.twig rename to workers/sender/src/providers/email/templates/components/workspace.twig diff --git a/workers/email/src/templates/emails/assignee/html.twig b/workers/sender/src/providers/email/templates/emails/assignee/html.twig similarity index 100% rename from workers/email/src/templates/emails/assignee/html.twig rename to workers/sender/src/providers/email/templates/emails/assignee/html.twig diff --git a/workers/email/src/templates/emails/assignee/subject.twig b/workers/sender/src/providers/email/templates/emails/assignee/subject.twig similarity index 100% rename from workers/email/src/templates/emails/assignee/subject.twig rename to workers/sender/src/providers/email/templates/emails/assignee/subject.twig diff --git a/workers/email/src/templates/emails/assignee/text.twig b/workers/sender/src/providers/email/templates/emails/assignee/text.twig similarity index 100% rename from workers/email/src/templates/emails/assignee/text.twig rename to workers/sender/src/providers/email/templates/emails/assignee/text.twig diff --git a/workers/email/src/templates/emails/block-workspace/html.twig b/workers/sender/src/providers/email/templates/emails/block-workspace/html.twig similarity index 100% rename from workers/email/src/templates/emails/block-workspace/html.twig rename to workers/sender/src/providers/email/templates/emails/block-workspace/html.twig diff --git a/workers/email/src/templates/emails/block-workspace/subject.twig b/workers/sender/src/providers/email/templates/emails/block-workspace/subject.twig similarity index 100% rename from workers/email/src/templates/emails/block-workspace/subject.twig rename to workers/sender/src/providers/email/templates/emails/block-workspace/subject.twig diff --git a/workers/email/src/templates/emails/block-workspace/text.twig b/workers/sender/src/providers/email/templates/emails/block-workspace/text.twig similarity index 100% rename from workers/email/src/templates/emails/block-workspace/text.twig rename to workers/sender/src/providers/email/templates/emails/block-workspace/text.twig diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig b/workers/sender/src/providers/email/templates/emails/blocked-workspace-reminder/html.twig similarity index 100% rename from workers/email/src/templates/emails/blocked-workspace-reminder/html.twig rename to workers/sender/src/providers/email/templates/emails/blocked-workspace-reminder/html.twig diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig b/workers/sender/src/providers/email/templates/emails/blocked-workspace-reminder/subject.twig similarity index 100% rename from workers/email/src/templates/emails/blocked-workspace-reminder/subject.twig rename to workers/sender/src/providers/email/templates/emails/blocked-workspace-reminder/subject.twig diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig b/workers/sender/src/providers/email/templates/emails/blocked-workspace-reminder/text.twig similarity index 100% rename from workers/email/src/templates/emails/blocked-workspace-reminder/text.twig rename to workers/sender/src/providers/email/templates/emails/blocked-workspace-reminder/text.twig diff --git a/workers/email/src/templates/emails/days-limit-almost-reached/html.twig b/workers/sender/src/providers/email/templates/emails/days-limit-almost-reached/html.twig similarity index 100% rename from workers/email/src/templates/emails/days-limit-almost-reached/html.twig rename to workers/sender/src/providers/email/templates/emails/days-limit-almost-reached/html.twig diff --git a/workers/email/src/templates/emails/days-limit-almost-reached/subject.twig b/workers/sender/src/providers/email/templates/emails/days-limit-almost-reached/subject.twig similarity index 100% rename from workers/email/src/templates/emails/days-limit-almost-reached/subject.twig rename to workers/sender/src/providers/email/templates/emails/days-limit-almost-reached/subject.twig diff --git a/workers/email/src/templates/emails/days-limit-almost-reached/text.twig b/workers/sender/src/providers/email/templates/emails/days-limit-almost-reached/text.twig similarity index 100% rename from workers/email/src/templates/emails/days-limit-almost-reached/text.twig rename to workers/sender/src/providers/email/templates/emails/days-limit-almost-reached/text.twig diff --git a/workers/email/src/templates/emails/event/html.twig b/workers/sender/src/providers/email/templates/emails/event/html.twig similarity index 100% rename from workers/email/src/templates/emails/event/html.twig rename to workers/sender/src/providers/email/templates/emails/event/html.twig diff --git a/workers/email/src/templates/emails/event/subject.twig b/workers/sender/src/providers/email/templates/emails/event/subject.twig similarity index 100% rename from workers/email/src/templates/emails/event/subject.twig rename to workers/sender/src/providers/email/templates/emails/event/subject.twig diff --git a/workers/email/src/templates/emails/event/text.twig b/workers/sender/src/providers/email/templates/emails/event/text.twig similarity index 100% rename from workers/email/src/templates/emails/event/text.twig rename to workers/sender/src/providers/email/templates/emails/event/text.twig diff --git a/workers/email/src/templates/emails/events-limit-almost-reached/html.twig b/workers/sender/src/providers/email/templates/emails/events-limit-almost-reached/html.twig similarity index 100% rename from workers/email/src/templates/emails/events-limit-almost-reached/html.twig rename to workers/sender/src/providers/email/templates/emails/events-limit-almost-reached/html.twig diff --git a/workers/email/src/templates/emails/events-limit-almost-reached/subject.twig b/workers/sender/src/providers/email/templates/emails/events-limit-almost-reached/subject.twig similarity index 100% rename from workers/email/src/templates/emails/events-limit-almost-reached/subject.twig rename to workers/sender/src/providers/email/templates/emails/events-limit-almost-reached/subject.twig diff --git a/workers/email/src/templates/emails/events-limit-almost-reached/text.twig b/workers/sender/src/providers/email/templates/emails/events-limit-almost-reached/text.twig similarity index 100% rename from workers/email/src/templates/emails/events-limit-almost-reached/text.twig rename to workers/sender/src/providers/email/templates/emails/events-limit-almost-reached/text.twig diff --git a/workers/email/src/templates/emails/password-reset/html.twig b/workers/sender/src/providers/email/templates/emails/password-reset/html.twig similarity index 100% rename from workers/email/src/templates/emails/password-reset/html.twig rename to workers/sender/src/providers/email/templates/emails/password-reset/html.twig diff --git a/workers/email/src/templates/emails/password-reset/subject.twig b/workers/sender/src/providers/email/templates/emails/password-reset/subject.twig similarity index 100% rename from workers/email/src/templates/emails/password-reset/subject.twig rename to workers/sender/src/providers/email/templates/emails/password-reset/subject.twig diff --git a/workers/email/src/templates/emails/password-reset/text.twig b/workers/sender/src/providers/email/templates/emails/password-reset/text.twig similarity index 100% rename from workers/email/src/templates/emails/password-reset/text.twig rename to workers/sender/src/providers/email/templates/emails/password-reset/text.twig diff --git a/workers/email/src/templates/emails/payment-failed/html.twig b/workers/sender/src/providers/email/templates/emails/payment-failed/html.twig similarity index 100% rename from workers/email/src/templates/emails/payment-failed/html.twig rename to workers/sender/src/providers/email/templates/emails/payment-failed/html.twig diff --git a/workers/email/src/templates/emails/payment-failed/subject.twig b/workers/sender/src/providers/email/templates/emails/payment-failed/subject.twig similarity index 100% rename from workers/email/src/templates/emails/payment-failed/subject.twig rename to workers/sender/src/providers/email/templates/emails/payment-failed/subject.twig diff --git a/workers/email/src/templates/emails/payment-failed/text.twig b/workers/sender/src/providers/email/templates/emails/payment-failed/text.twig similarity index 100% rename from workers/email/src/templates/emails/payment-failed/text.twig rename to workers/sender/src/providers/email/templates/emails/payment-failed/text.twig diff --git a/workers/email/src/templates/emails/payment-success/html.twig b/workers/sender/src/providers/email/templates/emails/payment-success/html.twig similarity index 100% rename from workers/email/src/templates/emails/payment-success/html.twig rename to workers/sender/src/providers/email/templates/emails/payment-success/html.twig diff --git a/workers/email/src/templates/emails/payment-success/subject.twig b/workers/sender/src/providers/email/templates/emails/payment-success/subject.twig similarity index 100% rename from workers/email/src/templates/emails/payment-success/subject.twig rename to workers/sender/src/providers/email/templates/emails/payment-success/subject.twig diff --git a/workers/email/src/templates/emails/payment-success/text.twig b/workers/sender/src/providers/email/templates/emails/payment-success/text.twig similarity index 100% rename from workers/email/src/templates/emails/payment-success/text.twig rename to workers/sender/src/providers/email/templates/emails/payment-success/text.twig diff --git a/workers/email/src/templates/emails/several-events/html.twig b/workers/sender/src/providers/email/templates/emails/several-events/html.twig similarity index 100% rename from workers/email/src/templates/emails/several-events/html.twig rename to workers/sender/src/providers/email/templates/emails/several-events/html.twig diff --git a/workers/email/src/templates/emails/several-events/subject.twig b/workers/sender/src/providers/email/templates/emails/several-events/subject.twig similarity index 100% rename from workers/email/src/templates/emails/several-events/subject.twig rename to workers/sender/src/providers/email/templates/emails/several-events/subject.twig diff --git a/workers/email/src/templates/emails/several-events/text.twig b/workers/sender/src/providers/email/templates/emails/several-events/text.twig similarity index 100% rename from workers/email/src/templates/emails/several-events/text.twig rename to workers/sender/src/providers/email/templates/emails/several-events/text.twig diff --git a/workers/email/src/templates/emails/sign-up/html.twig b/workers/sender/src/providers/email/templates/emails/sign-up/html.twig similarity index 100% rename from workers/email/src/templates/emails/sign-up/html.twig rename to workers/sender/src/providers/email/templates/emails/sign-up/html.twig diff --git a/workers/email/src/templates/emails/sign-up/subject.twig b/workers/sender/src/providers/email/templates/emails/sign-up/subject.twig similarity index 100% rename from workers/email/src/templates/emails/sign-up/subject.twig rename to workers/sender/src/providers/email/templates/emails/sign-up/subject.twig diff --git a/workers/email/src/templates/emails/sign-up/text.twig b/workers/sender/src/providers/email/templates/emails/sign-up/text.twig similarity index 100% rename from workers/email/src/templates/emails/sign-up/text.twig rename to workers/sender/src/providers/email/templates/emails/sign-up/text.twig diff --git a/workers/email/src/templates/emails/workspace-invite/html.twig b/workers/sender/src/providers/email/templates/emails/workspace-invite/html.twig similarity index 100% rename from workers/email/src/templates/emails/workspace-invite/html.twig rename to workers/sender/src/providers/email/templates/emails/workspace-invite/html.twig diff --git a/workers/email/src/templates/emails/workspace-invite/subject.twig b/workers/sender/src/providers/email/templates/emails/workspace-invite/subject.twig similarity index 100% rename from workers/email/src/templates/emails/workspace-invite/subject.twig rename to workers/sender/src/providers/email/templates/emails/workspace-invite/subject.twig diff --git a/workers/email/src/templates/emails/workspace-invite/text.twig b/workers/sender/src/providers/email/templates/emails/workspace-invite/text.twig similarity index 100% rename from workers/email/src/templates/emails/workspace-invite/text.twig rename to workers/sender/src/providers/email/templates/emails/workspace-invite/text.twig diff --git a/workers/email/src/templates/extensions.ts b/workers/sender/src/providers/email/templates/extensions.ts similarity index 100% rename from workers/email/src/templates/extensions.ts rename to workers/sender/src/providers/email/templates/extensions.ts diff --git a/workers/email/src/templates/index.ts b/workers/sender/src/providers/email/templates/index.ts similarity index 72% rename from workers/email/src/templates/index.ts rename to workers/sender/src/providers/email/templates/index.ts index 29c59965f..495c8d30b 100644 --- a/workers/email/src/templates/index.ts +++ b/workers/sender/src/providers/email/templates/index.ts @@ -38,14 +38,14 @@ const emailTemplatesPath = path.resolve(__dirname, emailTemplatesDir); * Return object like * { * 'new-event': { - * subject: '/Users/.../hawk.mono/workers/workers/email/src/templates/emails/new-event/subject.twig', - * html: '/Users/.../hawk.mono/workers/workers/email/src/templates/emails/new-event/html.twig', - * text: '/Users/.../hawk.mono/workers/workers/email/src/templates/emails/new-event/text.twig' + * subject: '/Users/.../workers/sender/src/providers/email/templates/emails/new-event/subject.twig', + * html: '/Users/.../workers/sender/src/providers/email/templates/emails/new-event/html.twig', + * text: '/Users/.../workers/sender/src/providers/email/templates/emails/new-event/text.twig' * }, * 'several-events': { - * subject: '/Users/.../hawk.mono/workers/workers/email/src/templates/emails/several-events/subject.twig', - * html: '/Users/.../hawk.mono/workers/workers/email/src/templates/emails/several-events/html.twig', - * text: '/Users/.../hawk.mono/workers/workers/email/src/templates/emails/several-events/text.twig' + * subject: '/Users/.../workers/sender/src/providers/email/templates/emails/several-events/subject.twig', + * html: '/Users/.../workers/sender/src/providers/email/templates/emails/several-events/html.twig', + * text: '/Users/.../workers/sender/src/providers/email/templates/emails/several-events/text.twig' * } * } */ diff --git a/workers/email/src/templates/names.ts b/workers/sender/src/providers/email/templates/names.ts similarity index 100% rename from workers/email/src/templates/names.ts rename to workers/sender/src/providers/email/templates/names.ts diff --git a/workers/loop/src/deliverer.ts b/workers/sender/src/providers/loop/deliverer.ts similarity index 100% rename from workers/loop/src/deliverer.ts rename to workers/sender/src/providers/loop/deliverer.ts diff --git a/workers/loop/src/provider.ts b/workers/sender/src/providers/loop/provider.ts similarity index 96% rename from workers/loop/src/provider.ts rename to workers/sender/src/providers/loop/provider.ts index d1143c6de..75494a6f2 100644 --- a/workers/loop/src/provider.ts +++ b/workers/sender/src/providers/loop/provider.ts @@ -1,7 +1,7 @@ import NotificationsProvider from 'hawk-worker-sender/src/provider'; import { Notification, EventsTemplateVariables } from 'hawk-worker-sender/types/template-variables'; import templates from './templates'; -import { LoopTemplate } from '../types/template'; +import { LoopTemplate } from './types/template'; import LoopDeliverer from './deliverer'; /** diff --git a/workers/loop/src/templates/event.ts b/workers/sender/src/providers/loop/templates/event.ts similarity index 96% rename from workers/loop/src/templates/event.ts rename to workers/sender/src/providers/loop/templates/event.ts index afc6622ee..731b93769 100644 --- a/workers/loop/src/templates/event.ts +++ b/workers/sender/src/providers/loop/templates/event.ts @@ -1,6 +1,6 @@ import { GroupedEventDBScheme } from '@hawk.so/types'; import type { EventsTemplateVariables, TemplateEventData } from 'hawk-worker-sender/types/template-variables'; -import { toMaxLen } from '../../../slack/src/templates/utils'; +import { toMaxLen } from '../../slack/templates/utils'; /** * Renders backtrace overview diff --git a/workers/loop/src/templates/index.ts b/workers/sender/src/providers/loop/templates/index.ts similarity index 100% rename from workers/loop/src/templates/index.ts rename to workers/sender/src/providers/loop/templates/index.ts diff --git a/workers/loop/src/templates/several-events.ts b/workers/sender/src/providers/loop/templates/several-events.ts similarity index 92% rename from workers/loop/src/templates/several-events.ts rename to workers/sender/src/providers/loop/templates/several-events.ts index bc7923060..a30052861 100644 --- a/workers/loop/src/templates/several-events.ts +++ b/workers/sender/src/providers/loop/templates/several-events.ts @@ -1,5 +1,5 @@ import type { EventsTemplateVariables } from 'hawk-worker-sender/types/template-variables'; -import { declOfNum } from '../../../../lib/utils/decl'; +import { declOfNum } from '../../../../../../lib/utils/decl'; /** * Return tpl with data substitutions diff --git a/workers/loop/types/template.d.ts b/workers/sender/src/providers/loop/types/template.d.ts similarity index 100% rename from workers/loop/types/template.d.ts rename to workers/sender/src/providers/loop/types/template.d.ts diff --git a/workers/slack/src/deliverer.ts b/workers/sender/src/providers/slack/deliverer.ts similarity index 100% rename from workers/slack/src/deliverer.ts rename to workers/sender/src/providers/slack/deliverer.ts diff --git a/workers/slack/src/provider.ts b/workers/sender/src/providers/slack/provider.ts similarity index 97% rename from workers/slack/src/provider.ts rename to workers/sender/src/providers/slack/provider.ts index c67ce7807..db1001095 100644 --- a/workers/slack/src/provider.ts +++ b/workers/sender/src/providers/slack/provider.ts @@ -2,7 +2,7 @@ import NotificationsProvider from 'hawk-worker-sender/src/provider'; import { Notification, EventsTemplateVariables } from 'hawk-worker-sender/types/template-variables'; import templates from './templates'; import { IncomingWebhookSendArguments } from '@slack/webhook'; -import { SlackTemplate } from '../types/template'; +import { SlackTemplate } from './types/template'; import SlackDeliverer from './deliverer'; /** diff --git a/workers/slack/src/templates/event.ts b/workers/sender/src/providers/slack/templates/event.ts similarity index 100% rename from workers/slack/src/templates/event.ts rename to workers/sender/src/providers/slack/templates/event.ts diff --git a/workers/slack/src/templates/index.ts b/workers/sender/src/providers/slack/templates/index.ts similarity index 100% rename from workers/slack/src/templates/index.ts rename to workers/sender/src/providers/slack/templates/index.ts diff --git a/workers/slack/src/templates/several-events.ts b/workers/sender/src/providers/slack/templates/several-events.ts similarity index 100% rename from workers/slack/src/templates/several-events.ts rename to workers/sender/src/providers/slack/templates/several-events.ts diff --git a/workers/slack/src/templates/utils.ts b/workers/sender/src/providers/slack/templates/utils.ts similarity index 100% rename from workers/slack/src/templates/utils.ts rename to workers/sender/src/providers/slack/templates/utils.ts diff --git a/workers/slack/types/template.d.ts b/workers/sender/src/providers/slack/types/template.d.ts similarity index 100% rename from workers/slack/types/template.d.ts rename to workers/sender/src/providers/slack/types/template.d.ts diff --git a/workers/telegram/src/provider.ts b/workers/sender/src/providers/telegram/provider.ts similarity index 96% rename from workers/telegram/src/provider.ts rename to workers/sender/src/providers/telegram/provider.ts index 9515404cb..274888777 100644 --- a/workers/telegram/src/provider.ts +++ b/workers/sender/src/providers/telegram/provider.ts @@ -1,7 +1,7 @@ import { EventsTemplateVariables, Notification } from 'hawk-worker-sender/types/template-variables'; import NotificationsProvider from 'hawk-worker-sender/src/provider'; import templates from './templates'; -import { TelegramTemplate } from '../types/template'; +import { TelegramTemplate } from './types/template'; import axios from 'axios'; /** diff --git a/workers/telegram/src/templates/event.ts b/workers/sender/src/providers/telegram/templates/event.ts similarity index 100% rename from workers/telegram/src/templates/event.ts rename to workers/sender/src/providers/telegram/templates/event.ts diff --git a/workers/telegram/src/templates/index.ts b/workers/sender/src/providers/telegram/templates/index.ts similarity index 100% rename from workers/telegram/src/templates/index.ts rename to workers/sender/src/providers/telegram/templates/index.ts diff --git a/workers/telegram/src/templates/several-events.ts b/workers/sender/src/providers/telegram/templates/several-events.ts similarity index 100% rename from workers/telegram/src/templates/several-events.ts rename to workers/sender/src/providers/telegram/templates/several-events.ts diff --git a/workers/telegram/types/template.d.ts b/workers/sender/src/providers/telegram/types/template.d.ts similarity index 100% rename from workers/telegram/types/template.d.ts rename to workers/sender/src/providers/telegram/types/template.d.ts diff --git a/workers/webhook/src/deliverer.ts b/workers/sender/src/providers/webhook/deliverer.ts similarity index 96% rename from workers/webhook/src/deliverer.ts rename to workers/sender/src/providers/webhook/deliverer.ts index 5e5a104bd..65612393a 100644 --- a/workers/webhook/src/deliverer.ts +++ b/workers/sender/src/providers/webhook/deliverer.ts @@ -1,8 +1,8 @@ import https from 'https'; import http from 'http'; import { createLogger, format, Logger, transports } from 'winston'; -import { WebhookDelivery } from '../types/template'; -import { HttpStatusCode, MS_IN_SEC } from '../../../lib/utils/consts'; +import { WebhookDelivery } from './types/template'; +import { HttpStatusCode, MS_IN_SEC } from '../../../../../lib/utils/consts'; /** * How many seconds to wait for a webhook response before aborting diff --git a/workers/webhook/src/provider.ts b/workers/sender/src/providers/webhook/provider.ts similarity index 100% rename from workers/webhook/src/provider.ts rename to workers/sender/src/providers/webhook/provider.ts diff --git a/workers/webhook/src/templates/generic.ts b/workers/sender/src/providers/webhook/templates/generic.ts similarity index 98% rename from workers/webhook/src/templates/generic.ts rename to workers/sender/src/providers/webhook/templates/generic.ts index f2fcabf3a..5076ed879 100644 --- a/workers/webhook/src/templates/generic.ts +++ b/workers/sender/src/providers/webhook/templates/generic.ts @@ -6,7 +6,7 @@ import { DecodedGroupedEvent, PlanDBScheme, } from '@hawk.so/types'; -import { WebhookDelivery } from '../../types/template'; +import { WebhookDelivery } from '../types/template'; /** * Projects safe public fields from a project document diff --git a/workers/webhook/src/templates/index.ts b/workers/sender/src/providers/webhook/templates/index.ts similarity index 100% rename from workers/webhook/src/templates/index.ts rename to workers/sender/src/providers/webhook/templates/index.ts diff --git a/workers/webhook/types/template.d.ts b/workers/sender/src/providers/webhook/types/template.d.ts similarity index 100% rename from workers/webhook/types/template.d.ts rename to workers/sender/src/providers/webhook/types/template.d.ts diff --git a/workers/email/tests/provider.test.ts b/workers/sender/tests/email/provider.test.ts similarity index 97% rename from workers/email/tests/provider.test.ts rename to workers/sender/tests/email/provider.test.ts index f83a59d07..2ab7b2d43 100644 --- a/workers/email/tests/provider.test.ts +++ b/workers/sender/tests/email/provider.test.ts @@ -11,9 +11,9 @@ nodemailerMock.createTransport = jest.fn(() => ({ jest.mock('nodemailer', () => nodemailerMock); import { DecodedGroupedEvent, ProjectDBScheme, UserDBScheme } from '@hawk.so/types'; -import '../src/env'; -import EmailProvider from '../src/provider'; -import Templates from '../src/templates/names'; +import '../../src/env'; +import EmailProvider from '../../src/providers/email/provider'; +import Templates from '../../src/providers/email/templates/names'; import { ObjectId } from 'mongodb'; describe('EmailProvider', () => { diff --git a/workers/loop/tests/__mocks__/event-notify.ts b/workers/sender/tests/loop/__mocks__/event-notify.ts similarity index 100% rename from workers/loop/tests/__mocks__/event-notify.ts rename to workers/sender/tests/loop/__mocks__/event-notify.ts diff --git a/workers/loop/tests/__mocks__/several-events-notify.ts b/workers/sender/tests/loop/__mocks__/several-events-notify.ts similarity index 100% rename from workers/loop/tests/__mocks__/several-events-notify.ts rename to workers/sender/tests/loop/__mocks__/several-events-notify.ts diff --git a/workers/loop/tests/provider.test.ts b/workers/sender/tests/loop/provider.test.ts similarity index 97% rename from workers/loop/tests/provider.test.ts rename to workers/sender/tests/loop/provider.test.ts index 955ad4911..2157bdfd0 100644 --- a/workers/loop/tests/provider.test.ts +++ b/workers/sender/tests/loop/provider.test.ts @@ -1,8 +1,8 @@ import { EventNotification, SeveralEventsNotification, EventsTemplateVariables } from 'hawk-worker-sender/types/template-variables'; import { DecodedGroupedEvent, ProjectDBScheme } from '@hawk.so/types'; -import LoopProvider from '../src/provider'; -import templates from '../src/templates'; -import SeveralEventsTpl from '../src/templates/several-events'; +import LoopProvider from '../../src/providers/loop/provider'; +import templates from '../../src/providers/loop/templates'; +import SeveralEventsTpl from '../../src/providers/loop/templates/several-events'; import EventNotifyMock from './__mocks__/event-notify'; import SeveralEventsNotifyMock from './__mocks__/several-events-notify'; import { ObjectId } from 'mongodb'; @@ -20,7 +20,7 @@ const deliver = jest.fn(); /** * Loop Deliverer mock */ -jest.mock('./../src/deliverer.ts', () => { +jest.mock('../../src/providers/loop/deliverer', () => { return jest.fn().mockImplementation(() => { /** * Now we can track calls to 'deliver' diff --git a/workers/sender/tests/sender-example.ts b/workers/sender/tests/sender-example.ts deleted file mode 100644 index 2c9adc742..000000000 --- a/workers/sender/tests/sender-example.ts +++ /dev/null @@ -1,27 +0,0 @@ -import SenderWorker from '../src'; -import ExampleProvider from './provider-example'; - -/** - * This class in an example implementation of the abstract Sender - * Used in tests. - * - * Will send messages to the terminal console ->> mister ConsoleSender - */ -export default class ExampleSenderWorker extends SenderWorker { - /** - * Worker type - */ - public readonly type: string = 'sender/example'; - - /** - * Email channel type - */ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - protected channelType = 'console'; - - /** - * Email provider - */ - protected provider = new ExampleProvider(); -} diff --git a/workers/slack/tests/__mocks__/event-notify.ts b/workers/sender/tests/slack/__mocks__/event-notify.ts similarity index 100% rename from workers/slack/tests/__mocks__/event-notify.ts rename to workers/sender/tests/slack/__mocks__/event-notify.ts diff --git a/workers/slack/tests/__mocks__/several-events-notify.ts b/workers/sender/tests/slack/__mocks__/several-events-notify.ts similarity index 100% rename from workers/slack/tests/__mocks__/several-events-notify.ts rename to workers/sender/tests/slack/__mocks__/several-events-notify.ts diff --git a/workers/slack/tests/provider.test.ts b/workers/sender/tests/slack/provider.test.ts similarity index 92% rename from workers/slack/tests/provider.test.ts rename to workers/sender/tests/slack/provider.test.ts index 2b4a84668..d5fa43811 100644 --- a/workers/slack/tests/provider.test.ts +++ b/workers/sender/tests/slack/provider.test.ts @@ -1,7 +1,7 @@ -import templates from '../src/templates'; +import templates from '../../src/providers/slack/templates'; import EventNotifyMock from './__mocks__/event-notify'; import SeveralEventsNotifyMock from './__mocks__/several-events-notify'; -import SlackProvider from '../src/provider'; +import SlackProvider from '../../src/providers/slack/provider'; /** * The sample of the Slack Incoming Webhook endpoint @@ -16,7 +16,7 @@ const deliver = jest.fn(); /** * Slack Deliverer mock */ -jest.mock('./../src/deliverer.ts', () => { +jest.mock('../../src/providers/slack/deliverer', () => { return jest.fn().mockImplementation(() => { /** * Now we can track calls to 'deliver' diff --git a/workers/slack/tests/utils.test.ts b/workers/sender/tests/slack/utils.test.ts similarity index 94% rename from workers/slack/tests/utils.test.ts rename to workers/sender/tests/slack/utils.test.ts index 7c67f75da..44961591e 100644 --- a/workers/slack/tests/utils.test.ts +++ b/workers/sender/tests/slack/utils.test.ts @@ -1,6 +1,6 @@ import { ObjectId } from 'mongodb'; import { ProjectDBScheme, GroupedEventDBScheme } from '@hawk.so/types'; -import { getEventUrl } from '../src/templates/utils'; +import { getEventUrl } from '../../src/providers/slack/templates/utils'; const project = { _id: new ObjectId('5d206f7f9aaf7c0071d64596'), diff --git a/workers/telegram/tests/provider.test.ts b/workers/sender/tests/telegram/provider.test.ts similarity index 96% rename from workers/telegram/tests/provider.test.ts rename to workers/sender/tests/telegram/provider.test.ts index 51ba45c4a..1c69e8213 100644 --- a/workers/telegram/tests/provider.test.ts +++ b/workers/sender/tests/telegram/provider.test.ts @@ -1,8 +1,8 @@ import { EventNotification, SeveralEventsNotification } from 'hawk-worker-sender/types/template-variables'; import { DecodedGroupedEvent, ProjectDBScheme } from '@hawk.so/types'; -import TelegramProvider from 'hawk-worker-telegram/src/provider'; -import templates from '../src/templates'; -import EventTpl from '../src/templates/event'; +import TelegramProvider from '../../src/providers/telegram/provider'; +import templates from '../../src/providers/telegram/templates'; +import EventTpl from '../../src/providers/telegram/templates/event'; import { ObjectId } from 'mongodb'; /** diff --git a/workers/webhook/tests/__mocks__/assignee-notify.ts b/workers/sender/tests/webhook/__mocks__/assignee-notify.ts similarity index 100% rename from workers/webhook/tests/__mocks__/assignee-notify.ts rename to workers/sender/tests/webhook/__mocks__/assignee-notify.ts diff --git a/workers/webhook/tests/__mocks__/event-notify.ts b/workers/sender/tests/webhook/__mocks__/event-notify.ts similarity index 100% rename from workers/webhook/tests/__mocks__/event-notify.ts rename to workers/sender/tests/webhook/__mocks__/event-notify.ts diff --git a/workers/webhook/tests/__mocks__/several-events-notify.ts b/workers/sender/tests/webhook/__mocks__/several-events-notify.ts similarity index 100% rename from workers/webhook/tests/__mocks__/several-events-notify.ts rename to workers/sender/tests/webhook/__mocks__/several-events-notify.ts diff --git a/workers/webhook/tests/provider.test.ts b/workers/sender/tests/webhook/provider.test.ts similarity index 99% rename from workers/webhook/tests/provider.test.ts rename to workers/sender/tests/webhook/provider.test.ts index 878d23358..d29fec2ae 100644 --- a/workers/webhook/tests/provider.test.ts +++ b/workers/sender/tests/webhook/provider.test.ts @@ -1,7 +1,7 @@ import EventNotifyMock from './__mocks__/event-notify'; import SeveralEventsNotifyMock from './__mocks__/several-events-notify'; import AssigneeNotifyMock from './__mocks__/assignee-notify'; -import WebhookProvider from '../src/provider'; +import WebhookProvider from '../../src/providers/webhook/provider'; import { Notification } from 'hawk-worker-sender/types/template-variables'; import { ObjectId } from 'mongodb'; @@ -18,7 +18,7 @@ const deliver = jest.fn(); /** * Webhook Deliverer mock */ -jest.mock('../src/deliverer', () => { +jest.mock('../../src/providers/webhook/deliverer', () => { return jest.fn().mockImplementation(() => { return { deliver: deliver, diff --git a/workers/sender/tests/worker.test.ts b/workers/sender/tests/worker.test.ts index ef5b05129..a02922199 100644 --- a/workers/sender/tests/worker.test.ts +++ b/workers/sender/tests/worker.test.ts @@ -145,22 +145,67 @@ describe('Sender Worker', () => { })); // eslint-disable-next-line @typescript-eslint/no-var-requires - const ExampleSenderWorker = require('./sender-example').default; + const SenderWorker = require('../src').default; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const ChannelSenderWorker = require('../src/channel-sender').default; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const ExampleProvider = require('./provider-example').default; + + /** + * Creates a single-channel worker with mocked db controllers and example provider + */ + const createChannelWorker = (): any => { + return new ChannelSenderWorker( + 'console' as any, + new ExampleProvider(), + new MockDBController() as any, + new MockDBController() as any + ); + }; /** * Check worker type */ it('should have correct worker type', () => { - const worker = new ExampleSenderWorker(); + const worker = new SenderWorker(); + + expect(worker.type).toBe('sender'); + }); + + /** + * Each channel worker should consume its own `sender/` queue + */ + it('should create channel workers with correct types', () => { + const worker = new SenderWorker(); + + expect(worker.channelWorkers.length).toBeGreaterThan(0); + worker.channelWorkers.forEach((channelWorker: any) => { + expect(channelWorker.type).toMatch(/^sender\/[-a-z]+$/); + }); + }); + + /** + * SENDER_CHANNELS controls the enabled channels + */ + it('should respect SENDER_CHANNELS env variable', () => { + process.env.SENDER_CHANNELS = 'telegram, slack'; + + const worker = new SenderWorker(); + + expect(worker.channelWorkers.map((channelWorker: any) => channelWorker.type)).toEqual(['sender/telegram', 'sender/slack']); + + process.env.SENDER_CHANNELS = 'smoke-signals'; + + expect(() => new SenderWorker()).toThrow(/Unknown channels/); - expect(worker.type).toMatch(/^sender\/[-a-z]+$/); + delete process.env.SENDER_CHANNELS; }); /** * Check start and finish */ it('should start and finish without errors', async () => { - const worker = new ExampleSenderWorker(); + const worker = new SenderWorker(); await worker.start(); await worker.finish(); @@ -175,7 +220,7 @@ describe('Sender Worker', () => { */ it('should connect to db on start', async () => { dbConnectMock.mockClear(); - const worker = new ExampleSenderWorker(); + const worker = new SenderWorker(); await worker.start(); @@ -190,7 +235,7 @@ describe('Sender Worker', () => { * On 'handle' it should get Project from DB */ it('should query project on handle', async () => { - const worker = new ExampleSenderWorker(); + const worker = createChannelWorker(); await worker.handle({ type: 'event', @@ -211,7 +256,7 @@ describe('Sender Worker', () => { * Then, it should get events */ it('should query events on handle', async () => { - const worker = new ExampleSenderWorker(); + const worker = createChannelWorker(); await worker.handle({ type: 'event', @@ -232,7 +277,7 @@ describe('Sender Worker', () => { * Then, compute events count */ it('should query daily events count on handle', async () => { - const worker = new ExampleSenderWorker(); + const worker = createChannelWorker(); await worker.handle({ type: 'event', @@ -258,7 +303,7 @@ describe('Sender Worker', () => { * crash in the fire-and-forget case; await still observes the rejection. */ it('should reject handle when provider.send fails', async () => { - const worker = new ExampleSenderWorker(); + const worker = createChannelWorker(); const sendError = new Error('provider send failed'); (worker as any).provider.send = jest.fn(() => { diff --git a/workers/slack/package.json b/workers/slack/package.json deleted file mode 100644 index c9ec59251..000000000 --- a/workers/slack/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "hawk-worker-slack", - "version": "1.0.0", - "description": "", - "main": "src/index.ts", - "license": "MIT", - "workerType": "sender/slack", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { - "@slack/webhook": "^5.0.3", - "json-templater": "^1.2.0", - "slack-block-kit": "^0.9.9" - } -} diff --git a/workers/slack/src/index.ts b/workers/slack/src/index.ts deleted file mode 100644 index 9d03d3b54..000000000 --- a/workers/slack/src/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as pkg from './../package.json'; -import SlackProvider from './provider'; -import SenderWorker from 'hawk-worker-sender/src'; -import { ChannelType } from 'hawk-worker-notifier/types/channel'; - -/** - * Worker to send email notifications - */ -export default class SlackSenderWorker extends SenderWorker { - /** - * Worker type - */ - public readonly type: string = pkg.workerType; - - /** - * Email channel type - */ - protected channelType = ChannelType.Slack; - - /** - * Email provider - */ - protected provider = new SlackProvider(); -} diff --git a/workers/slack/yarn.lock b/workers/slack/yarn.lock deleted file mode 100644 index 1f72f556a..000000000 --- a/workers/slack/yarn.lock +++ /dev/null @@ -1,48 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@slack/types@^1.2.1": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@slack/types/-/types-1.5.0.tgz#5c2cb0f718689266ff295aad33301d489272c842" - integrity sha512-oCYgatJYxHf9wE3tKXzOLeeTsF0ghX1TIcguNfVmO2V6NDe+cHAzZRglEOmJLdRINDS5gscAgSkeZpDhpKBeUA== - -"@slack/webhook@^5.0.3": - version "5.0.3" - resolved "https://registry.yarnpkg.com/@slack/webhook/-/webhook-5.0.3.tgz#2205cba9a8d49d2ae84ca93f11ab4a1dba2f963b" - integrity sha512-51vnejJ2zABNumPVukOLyerpHQT39/Lt0TYFtOEz/N2X77bPofOgfPj2atB3etaM07mxWHLT9IRJ4Zuqx38DkQ== - dependencies: - "@slack/types" "^1.2.1" - "@types/node" ">=8.9.0" - axios "^0.19.0" - -"@types/node@>=8.9.0": - version "13.11.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b" - integrity sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ== - -axios@^0.19.0: - version "0.19.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" - integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== - dependencies: - follow-redirects "1.5.10" - -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== - dependencies: - debug "=3.1.0" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= diff --git a/workers/telegram/README.md b/workers/telegram/README.md deleted file mode 100644 index 68d3ee9d3..000000000 --- a/workers/telegram/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Telegram worker - -Sends messages to the telegram chat diff --git a/workers/telegram/package.json b/workers/telegram/package.json deleted file mode 100644 index cb9c1a2ed..000000000 --- a/workers/telegram/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "hawk-worker-telegram", - "version": "0.0.1", - "main": "src/index.ts", - "license": "UNLICENSED", - "workerType": "sender/telegram", - "scripts": {}, - "dependencies": { - "axios": "^0.21.2" - }, - "devDependencies": {} -} diff --git a/workers/telegram/src/index.ts b/workers/telegram/src/index.ts deleted file mode 100644 index ffa1e7139..000000000 --- a/workers/telegram/src/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as pkg from '../package.json'; -import TelegramProvider from './provider'; -import SenderWorker from 'hawk-worker-sender/src'; -import { ChannelType } from 'hawk-worker-notifier/types/channel'; - -/** - * Worker to send email notifications - */ -export default class TelegramSenderWorker extends SenderWorker { - /** - * Worker type - */ - public readonly type: string = pkg.workerType; - - /** - * Telegram channel type - */ - protected channelType = ChannelType.Telegram; - - /** - * Telegram provider - */ - protected provider = new TelegramProvider(); -} diff --git a/workers/webhook/package.json b/workers/webhook/package.json deleted file mode 100644 index 91c8035d2..000000000 --- a/workers/webhook/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "hawk-worker-webhook", - "version": "1.0.0", - "description": "Webhook sender worker — delivers event notifications as JSON POST requests", - "main": "src/index.ts", - "license": "MIT", - "workerType": "sender/webhook", - "scripts": { - "test": "jest" - } -} diff --git a/workers/webhook/src/index.ts b/workers/webhook/src/index.ts deleted file mode 100644 index f826bb02e..000000000 --- a/workers/webhook/src/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as pkg from './../package.json'; -import WebhookProvider from './provider'; -import SenderWorker from 'hawk-worker-sender/src'; -import { ChannelType } from 'hawk-worker-notifier/types/channel'; - -/** - * Worker to send webhook notifications - */ -export default class WebhookSenderWorker extends SenderWorker { - /** - * Worker type - */ - public readonly type: string = pkg.workerType; - - /** - * Webhook channel type - */ - protected channelType = ChannelType.Webhook; - - /** - * Webhook provider - */ - protected provider = new WebhookProvider(); -}