Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions lib/utils/decl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Decl of number
*
* @param value - value to decl
* @param titles - titles to decl: ['новое событие', 'новых события', 'новых событий']
* @example declOfNum(1, ['новое событие', 'новых события', 'новых событий']) -> 'новое событие'
* @example declOfNum(2, ['новое событие', 'новых события', 'новых событий']) -> 'новых события'
* @example declOfNum(10, ['новое событие', 'новых события', 'новых событий']) -> 'новых событий'
* @example declOfNum(21, ['новое событие', 'новых события', 'новых событий']) -> 'новое событие'
* @returns decl of number
*/
export function declOfNum(value: number, titles: string[]): string {
const decimalBase = 10;
const hundredBase = 100;
const minExclusiveTeens = 4;
const maxExclusiveTeens = 20;
const manyFormIndex = 2;
const maxCaseIndex = 5;
const declCases = [manyFormIndex, 0, 1, 1, 1, manyFormIndex];

const valueModHundred = value % hundredBase;
const valueModTen = value % decimalBase;
const isTeens = valueModHundred > minExclusiveTeens && valueModHundred < maxExclusiveTeens;
const caseIndex = isTeens
? manyFormIndex
: declCases[valueModTen < maxCaseIndex ? valueModTen : maxCaseIndex];

return titles[caseIndex];
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"test:javascript": "jest workers/javascript",
"test:release": "jest workers/release",
"test:slack": "jest workers/slack",
"test:loop": "jest workers/loop",
"test:limiter": "jest workers/limiter --runInBand",
"test:grouper": "jest workers/grouper",
"test:diff": "jest ./workers/grouper/tests/diff.test.ts",
Expand All @@ -37,6 +38,7 @@
"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-grouper": "yarn worker hawk-worker-grouper",
"run-archiver": "yarn worker hawk-worker-archiver",
"run-accountant": "yarn worker hawk-worker-accountant",
Expand Down
15 changes: 15 additions & 0 deletions workers/loop/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"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"
}
}
47 changes: 47 additions & 0 deletions workers/loop/src/deliverer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { IncomingWebhook } from '@slack/webhook';
import { createLogger, format, Logger, transports } from 'winston';

/**
* Deliverer is the man who will send messages to external service
* Separated from the provider to allow testing 'send' method
* Loop is Slack-like platform, so we use Slack API to send messages.
*
*/
export default class LoopDeliverer {
/**
* Logger module
* (default level='info')
*/
private logger: Logger = createLogger({
level: process.env.LOG_LEVEL || 'info',
transports: [
new transports.Console({
format: format.combine(
format.timestamp(),
format.colorize(),
format.simple(),
format.printf((msg) => `${msg.timestamp} - ${msg.level}: ${msg.message}`)
),
}),
],
});

/**
* Sends message to the Loop through the Incoming Webhook app
* https://developers.loop.ru/integrate/webhooks/incoming/
*
* @param endpoint - where to send
* @param message - what to send
*/
public async deliver(endpoint: string, message: string): Promise<void> {
try {
const webhook = new IncomingWebhook(endpoint, {
username: 'Hawk',
});

await webhook.send(message);
} catch (e) {
this.logger.log('error', 'Can\'t deliver Incoming Webhook. Loop returns an error: ', e);
}
}
}
24 changes: 24 additions & 0 deletions workers/loop/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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();
}
44 changes: 44 additions & 0 deletions workers/loop/src/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 LoopDeliverer from './deliverer';

/**
* This class provides a 'send' method that will renders and sends a notification
*/
export default class LoopProvider extends NotificationsProvider {
/**
* Class with the 'deliver' method for sending messages to the Loop
*/
private readonly deliverer: LoopDeliverer;

/**
* Constructor allows to separate dependencies that can't be tested,
* so in tests they will be mocked.
*/
constructor() {
super();

this.deliverer = new LoopDeliverer();
}

/**
* Send loop message to recipient
*
* @param to - recipient endpoint
* @param notification - notification with payload and type
*/
public async send(to: string, notification: Notification): Promise<void> {
let template: LoopTemplate;

switch (notification.type) {
case 'event': template = templates.EventTpl; break;
case 'several-events':template = templates.SeveralEventsTpl; break;
}

const message = template(notification.payload as EventsTemplateVariables);

await this.deliverer.deliver(to, message);
}
}
57 changes: 57 additions & 0 deletions workers/loop/src/templates/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { GroupedEventDBScheme } from '@hawk.so/types';
import type { EventsTemplateVariables, TemplateEventData } from 'hawk-worker-sender/types/template-variables';
import { toMaxLen } from '../../../slack/src/templates/utils';

/**
* Renders backtrace overview
*
* @param event - event to render
*/
function renderBacktrace(event: GroupedEventDBScheme): string {
let code = '';

const firstNotEmptyFrame = event.payload.backtrace.find(frame => !!frame.sourceCode);

if (!firstNotEmptyFrame) {
return code;
}

code = firstNotEmptyFrame.sourceCode.map(({ line, content }) => {
let colDelimiter = ': ';

if (line === firstNotEmptyFrame.line) {
colDelimiter = ' ->';
}

const MAX_SOURCE_CODE_LINE_LENGTH = 65;

return `${line}${colDelimiter} ${toMaxLen(content, MAX_SOURCE_CODE_LINE_LENGTH)}`;
}).join('\n');

return code;
}

/**
* Return tpl with data substitutions
*
* @param tplData - event template data
*/
export default function render(tplData: EventsTemplateVariables): string {
const eventInfo = tplData.events[0] as TemplateEventData;
const event = eventInfo.event;
const eventURL = tplData.host + '/project/' + tplData.project._id + '/event/' + event._id + '/';
let location = 'Неизвестное место';

if (event.payload.backtrace && event.payload.backtrace.length > 0) {
location = event.payload.backtrace[0].file;
}

return ''.concat(
`**${event.payload.title}**`,
'\n',
`*${location}*\n`,
'```\n' + renderBacktrace(event) + '\n```',
'\n',
`[Посмотреть подробности](${eventURL}) `, `| *${tplData.project.name}*`, ` | ${eventInfo.newCount} новых (${eventInfo.event.totalCount} всего)`
);
}
7 changes: 7 additions & 0 deletions workers/loop/src/templates/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import EventTpl from './event';
import SeveralEventsTpl from './several-events';

export default {
EventTpl,
SeveralEventsTpl,
};
23 changes: 23 additions & 0 deletions workers/loop/src/templates/several-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { EventsTemplateVariables } from 'hawk-worker-sender/types/template-variables';
import { declOfNum } from '../../../../lib/utils/decl';

/**
* Return tpl with data substitutions
*
* @param tplData - event template data
*/
export default function render(tplData: EventsTemplateVariables): string {
const projectUrl = tplData.host + '/project/' + tplData.project._id;
let message = tplData.events.length + ' ' + declOfNum(
tplData.events.length,
['новое событие', 'новых события', 'новых событий']
) + '\n\n';

tplData.events.forEach(({ event, newCount }) => {
message += `(${newCount}) ${event.payload.title} \n`;
});

message += `\n[Посмотреть все события](${projectUrl}) | *${tplData.project.name}*`;

return message;
}
43 changes: 43 additions & 0 deletions workers/loop/tests/__mocks__/event-notify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { EventNotification } from 'hawk-worker-sender/types/template-variables';
import { ObjectId } from 'mongodb';

/**
* Example of new-events notify template variables
*/
export default {
type: 'event',
payload: {
events: [
{
event: {
totalCount: 10,
timestamp: Date.now(),
payload: {
title: 'New event',
backtrace: [ {
file: 'file',
line: 1,
sourceCode: [ {
line: 1,
content: 'code',
} ],
} ],
},
},
daysRepeated: 1,
newCount: 1,
},
],
period: 60,
host: process.env.GARAGE_URL,
hostOfStatic: process.env.API_STATIC_URL,
project: {
_id: new ObjectId('5d206f7f9aaf7c0071d64596'),
token: 'project-token',
name: 'Project',
workspaceId: new ObjectId('5d206f7f9aaf7c0071d64596'),
uidAdded: new ObjectId('5d206f7f9aaf7c0071d64596'),
notifications: [],
},
},
} as EventNotification;
63 changes: 63 additions & 0 deletions workers/loop/tests/__mocks__/several-events-notify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { SeveralEventsNotification } from 'hawk-worker-sender/types/template-variables';
import { GroupedEventDBScheme } from '@hawk.so/types';
import { ObjectId } from 'mongodb';

/**
* Example of several-events notify template variables
*/
export default {
type: 'several-events',
payload: {
events: [
{
event: {
totalCount: 10,
timestamp: Date.now(),
payload: {
title: 'New event',
backtrace: [ {
file: 'file',
line: 1,
sourceCode: [ {
line: 1,
content: 'code',
} ],
} ],
},
} as GroupedEventDBScheme,
daysRepeated: 1,
newCount: 1,
},
{
event: {
totalCount: 5,
payload: {
title: 'New event 2',
timestamp: Date.now(),
backtrace: [ {
file: 'file',
line: 1,
sourceCode: [ {
line: 1,
content: 'code',
} ],
} ],
},
},
daysRepeated: 100,
newCount: 1,
},
],
period: 60,
host: process.env.GARAGE_URL,
hostOfStatic: process.env.API_STATIC_URL,
project: {
_id: new ObjectId('5d206f7f9aaf7c0071d64596'),
token: 'project-token',
name: 'Project',
workspaceId: new ObjectId('5d206f7f9aaf7c0071d64596'),
uidAdded: new ObjectId('5d206f7f9aaf7c0071d64596'),
notifications: [],
},
},
} as SeveralEventsNotification;
Loading
Loading