Skip to content

Commit 312676a

Browse files
committed
feat: add hawk-worker-webhook service and update package.json
1 parent 1ce34ea commit 312676a

11 files changed

Lines changed: 259 additions & 3 deletions

File tree

docker-compose.dev.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,18 @@ services:
158158
- ./:/usr/src/app
159159
- workers-deps:/usr/src/app/node_modules
160160

161+
hawk-worker-webhook:
162+
build:
163+
dockerfile: "dev.Dockerfile"
164+
context: .
165+
env_file:
166+
- .env
167+
restart: unless-stopped
168+
entrypoint: yarn run-webhook
169+
volumes:
170+
- ./:/usr/src/app
171+
- workers-deps:/usr/src/app/node_modules
172+
161173
volumes:
162174
workers-deps:
163175

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "hawk.workers",
33
"private": true,
4-
"version": "0.1.2",
4+
"version": "0.1.3",
55
"description": "Hawk workers",
66
"repository": "git@github.com:codex-team/hawk.workers.git",
77
"license": "BUSL-1.1",
@@ -49,13 +49,14 @@
4949
"run-email": "yarn worker hawk-worker-email",
5050
"run-telegram": "yarn worker hawk-worker-telegram",
5151
"run-limiter": "yarn worker hawk-worker-limiter",
52-
"run-task-manager": "yarn worker hawk-worker-task-manager"
52+
"run-task-manager": "yarn worker hawk-worker-task-manager",
53+
"run-webhook": "yarn worker hawk-worker-webhook"
5354
},
5455
"dependencies": {
5556
"@babel/parser": "^7.26.9",
5657
"@babel/traverse": "7.26.9",
5758
"@hawk.so/nodejs": "^3.1.1",
58-
"@hawk.so/types": "^0.5.7",
59+
"@hawk.so/types": "^0.5.9",
5960
"@types/amqplib": "^0.8.2",
6061
"@types/jest": "^29.5.14",
6162
"@types/mongodb": "^3.5.15",

workers/notifier/types/channel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export enum ChannelType {
66
Telegram = 'telegram',
77
Slack = 'slack',
88
Loop = 'loop',
9+
Webhook = 'webhook',
910
}
1011

1112
/**

workers/webhook/package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "hawk-worker-webhook",
3+
"version": "1.0.0",
4+
"description": "Webhook sender worker — delivers event notifications as JSON POST requests",
5+
"main": "src/index.ts",
6+
"license": "MIT",
7+
"workerType": "sender/webhook",
8+
"scripts": {
9+
"test": "echo \"Error: no test specified\" && exit 1"
10+
}
11+
}

workers/webhook/src/deliverer.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { createLogger, format, Logger, transports } from 'winston';
2+
import { WebhookPayload } from '../types/template';
3+
4+
/**
5+
* Deliverer sends JSON POST requests to external webhook endpoints
6+
*/
7+
export default class WebhookDeliverer {
8+
/**
9+
* Logger module
10+
* (default level='info')
11+
*/
12+
private logger: Logger = createLogger({
13+
level: process.env.LOG_LEVEL || 'info',
14+
transports: [
15+
new transports.Console({
16+
format: format.combine(
17+
format.timestamp(),
18+
format.colorize(),
19+
format.simple(),
20+
format.printf((msg) => `${msg.timestamp} - ${msg.level}: ${msg.message}`)
21+
),
22+
}),
23+
],
24+
});
25+
26+
/**
27+
* Sends JSON payload to the webhook endpoint via HTTP POST
28+
*
29+
* @param endpoint - URL to POST to
30+
* @param payload - JSON body to send
31+
*/
32+
public async deliver(endpoint: string, payload: WebhookPayload): Promise<void> {
33+
const body = JSON.stringify(payload);
34+
35+
try {
36+
const response = await fetch(endpoint, {
37+
method: 'POST',
38+
headers: {
39+
'Content-Type': 'application/json',
40+
'User-Agent': 'Hawk-Webhook/1.0',
41+
},
42+
body,
43+
signal: AbortSignal.timeout(10_000),
44+
});
45+
46+
if (!response.ok) {
47+
this.logger.log('error', `Webhook delivery failed: ${response.status} ${response.statusText} for ${endpoint}`);
48+
}
49+
} catch (e) {
50+
this.logger.log('error', `Can't deliver webhook to ${endpoint}: `, e);
51+
}
52+
}
53+
}

workers/webhook/src/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as pkg from './../package.json';
2+
import WebhookProvider from './provider';
3+
import SenderWorker from 'hawk-worker-sender/src';
4+
import { ChannelType } from 'hawk-worker-notifier/types/channel';
5+
6+
/**
7+
* Worker to send webhook notifications
8+
*/
9+
export default class WebhookSenderWorker extends SenderWorker {
10+
/**
11+
* Worker type
12+
*/
13+
public readonly type: string = pkg.workerType;
14+
15+
/**
16+
* Webhook channel type
17+
*/
18+
protected channelType = ChannelType.Webhook;
19+
20+
/**
21+
* Webhook provider
22+
*/
23+
protected provider = new WebhookProvider();
24+
}

workers/webhook/src/provider.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import NotificationsProvider from 'hawk-worker-sender/src/provider';
2+
import { Notification, EventsTemplateVariables } from 'hawk-worker-sender/types/template-variables';
3+
import templates from './templates';
4+
import { WebhookPayload } from '../types/template';
5+
import WebhookDeliverer from './deliverer';
6+
7+
/**
8+
* This class provides a 'send' method that renders and sends a webhook notification
9+
*/
10+
export default class WebhookProvider extends NotificationsProvider {
11+
/**
12+
* Class with the 'deliver' method for sending HTTP POST requests
13+
*/
14+
private readonly deliverer: WebhookDeliverer;
15+
16+
constructor() {
17+
super();
18+
19+
this.deliverer = new WebhookDeliverer();
20+
}
21+
22+
/**
23+
* Send webhook notification to recipient
24+
*
25+
* @param to - recipient endpoint URL
26+
* @param notification - notification with payload and type
27+
*/
28+
public async send(to: string, notification: Notification): Promise<void> {
29+
let template: (tplData: EventsTemplateVariables) => WebhookPayload;
30+
31+
switch (notification.type) {
32+
case 'event': template = templates.EventTpl; break;
33+
case 'several-events': template = templates.SeveralEventsTpl; break;
34+
default: return;
35+
}
36+
37+
const payload = template(notification.payload as EventsTemplateVariables);
38+
39+
await this.deliverer.deliver(to, payload);
40+
}
41+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { EventsTemplateVariables, TemplateEventData } from 'hawk-worker-sender/types/template-variables';
2+
import { WebhookPayload } from '../../types/template';
3+
4+
/**
5+
* Builds webhook JSON payload for a single event notification
6+
*
7+
* @param tplData - event template data
8+
*/
9+
export default function render(tplData: EventsTemplateVariables): WebhookPayload {
10+
const eventInfo = tplData.events[0] as TemplateEventData;
11+
const event = eventInfo.event;
12+
const eventURL = tplData.host + '/project/' + tplData.project._id + '/event/' + event._id + '/';
13+
14+
let location: string | null = null;
15+
16+
if (event.payload.backtrace && event.payload.backtrace.length > 0 && event.payload.backtrace[0].file) {
17+
location = event.payload.backtrace[0].file;
18+
}
19+
20+
return {
21+
type: 'event',
22+
project: {
23+
id: tplData.project._id.toString(),
24+
name: tplData.project.name,
25+
},
26+
events: [
27+
{
28+
id: event._id.toString(),
29+
title: event.payload.title,
30+
newCount: eventInfo.newCount,
31+
totalCount: event.totalCount,
32+
url: eventURL,
33+
location,
34+
daysRepeated: eventInfo.daysRepeated,
35+
},
36+
],
37+
};
38+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import EventTpl from './event';
2+
import SeveralEventsTpl from './several-events';
3+
4+
export default {
5+
EventTpl,
6+
SeveralEventsTpl,
7+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { EventsTemplateVariables } from 'hawk-worker-sender/types/template-variables';
2+
import { WebhookPayload } from '../../types/template';
3+
4+
/**
5+
* Builds webhook JSON payload for a several-events notification
6+
*
7+
* @param tplData - event template data
8+
*/
9+
export default function render(tplData: EventsTemplateVariables): WebhookPayload {
10+
const projectUrl = tplData.host + '/project/' + tplData.project._id;
11+
12+
return {
13+
type: 'several-events',
14+
project: {
15+
id: tplData.project._id.toString(),
16+
name: tplData.project.name,
17+
url: projectUrl,
18+
},
19+
events: tplData.events.map(({ event, newCount, daysRepeated }) => {
20+
const eventURL = tplData.host + '/project/' + tplData.project._id + '/event/' + event._id + '/';
21+
22+
let location: string | null = null;
23+
24+
if (event.payload.backtrace && event.payload.backtrace.length > 0 && event.payload.backtrace[0].file) {
25+
location = event.payload.backtrace[0].file;
26+
}
27+
28+
return {
29+
id: event._id.toString(),
30+
title: event.payload.title,
31+
newCount,
32+
totalCount: event.totalCount,
33+
url: eventURL,
34+
location,
35+
daysRepeated,
36+
};
37+
}),
38+
period: tplData.period,
39+
};
40+
}

0 commit comments

Comments
 (0)