From 726596ce125062c934fff8d15be729ec495df7fb Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Wed, 9 Nov 2022 18:43:18 +0700 Subject: [PATCH 01/11] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=20help=20=D1=81?= =?UTF-8?q?=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B8=D0=B7?= =?UTF-8?q?=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=87=D0=B8=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=20=D0=B8?= =?UTF-8?q?=D0=B7=20telegram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 4 ++-- ...urrent-issues-eccm.bot-handler.service.ts} | 18 ++++++++++++++---- src/telegram-bot/telegram-bot.service.ts | 19 +++++++++++++------ .../telegram.bot-handler.interface.ts | 7 +++++++ 4 files changed, 36 insertions(+), 12 deletions(-) rename src/telegram-bot/handlers/{current-issues.bot-handler.service.ts => current-issues-eccm.bot-handler.service.ts} (75%) create mode 100644 src/telegram-bot/telegram.bot-handler.interface.ts diff --git a/src/app.module.ts b/src/app.module.ts index d2cddd0..35b18cf 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -24,7 +24,7 @@ import { UserMetaInfo } from './couchdb-datasources/user-meta-info'; import { PersonalNotificationAdapterService } from './notifications/adapters/personal-notification.adapter/personal-notification.adapter.service'; import { StatusChangeAdapterService } from './notifications/adapters/status-change.adapter.service'; import { CurrentIssuesEccmReportService } from './reports/current-issues-eccm.report.service'; -import { CurrentIssuesBotHandlerService } from './telegram-bot/handlers/current-issues.bot-handler.service'; +import { CurrentIssuesEccmBotHandlerService } from './telegram-bot/handlers/current-issues-eccm.bot-handler.service'; import { CurrentIssuesEccmReportController } from './reports/current-issues-eccm.report.controller'; import { DailyEccmReportController } from './reports/daily-eccm.report.controller'; import { DailyEccmReportService } from './reports/daily-eccm.report.service'; @@ -61,7 +61,7 @@ import { ChangesService } from './changes/changes.service'; PersonalNotificationAdapterService, StatusChangeAdapterService, CurrentIssuesEccmReportService, - CurrentIssuesBotHandlerService, + CurrentIssuesEccmBotHandlerService, DailyEccmReportService, ChangesService, ], diff --git a/src/telegram-bot/handlers/current-issues.bot-handler.service.ts b/src/telegram-bot/handlers/current-issues-eccm.bot-handler.service.ts similarity index 75% rename from src/telegram-bot/handlers/current-issues.bot-handler.service.ts rename to src/telegram-bot/handlers/current-issues-eccm.bot-handler.service.ts index 78f9e60..2392ccc 100644 --- a/src/telegram-bot/handlers/current-issues.bot-handler.service.ts +++ b/src/telegram-bot/handlers/current-issues-eccm.bot-handler.service.ts @@ -2,17 +2,23 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import TelegramBot from 'node-telegram-bot-api'; import { EccmConfig } from 'src/models/eccm-config.model'; -import { CurrentIssuesEccmReport, CurrentIssuesEccmReportService } from 'src/reports/current-issues-eccm.report.service'; +import { + CurrentIssuesEccmReport, + CurrentIssuesEccmReportService, +} from 'src/reports/current-issues-eccm.report.service'; import { UserMetaInfoService } from 'src/user-meta-info/user-meta-info.service'; import { TelegramBotService } from '../telegram-bot.service'; +import { TelegramBotHandlerInterface } from '../telegram.bot-handler.interface'; @Injectable() -export class CurrentIssuesBotHandlerService { - private forName = /\/current_issues_eccm (.+) (.+)/; +export class CurrentIssuesEccmBotHandlerService + implements TelegramBotHandlerInterface +{ + private forName = /\/current_issues_eccm (.+)/; private forCurrentUser = /\/current_issues_eccm/; private service: TelegramBotService; private eccmConfig: EccmConfig.Config; - private logger = new Logger(CurrentIssuesBotHandlerService.name); + private logger = new Logger(CurrentIssuesEccmBotHandlerService.name); constructor( private configService: ConfigService, @@ -44,4 +50,8 @@ export class CurrentIssuesBotHandlerService { }); }); } + + getHelpMsg(): string { + return '/current_issues_eccm - ваши текущие задачи из проекта ECCM'; + } } diff --git a/src/telegram-bot/telegram-bot.service.ts b/src/telegram-bot/telegram-bot.service.ts index 37da5fa..5c5dfe2 100644 --- a/src/telegram-bot/telegram-bot.service.ts +++ b/src/telegram-bot/telegram-bot.service.ts @@ -5,7 +5,8 @@ import TelegramBot from 'node-telegram-bot-api'; import { UserMetaInfoService } from 'src/user-meta-info/user-meta-info.service'; import axios from 'axios'; import { UserMetaInfoModel } from 'src/models/user-meta-info.model'; -import { CurrentIssuesBotHandlerService } from './handlers/current-issues.bot-handler.service'; +import { CurrentIssuesEccmBotHandlerService } from './handlers/current-issues-eccm.bot-handler.service'; +import { TelegramBotHandlerInterface } from './telegram.bot-handler.interface'; @Injectable() export class TelegramBotService { @@ -16,16 +17,19 @@ export class TelegramBotService { private registerRe = /\/register (\d+) (.+)/; + private handlers: TelegramBotHandlerInterface[] = []; + constructor( private userMetaInfoService: UserMetaInfoService, private usersService: UsersService, private configService: ConfigService, - private currentIssuesBotHandlerService: CurrentIssuesBotHandlerService, + private currentIssuesBotHandlerService: CurrentIssuesEccmBotHandlerService, ) { this.telegramBotToken = this.configService.get('telegramBotToken'); this.redminePublicUrlPrefix = this.configService.get('redmineUrlPublic'); this.initTelegramBot(); + this.handlers.push(this.currentIssuesBotHandlerService); } private async initTelegramBot(): Promise { @@ -51,19 +55,22 @@ export class TelegramBotService { msg.chat.id, ); let helpMessage: string; + /* eslint-disable */ if (userMetaInfo) { - // eslint-disable-next-line prettier/prettier + const handlersMessages = this.handlers + .map((handler) => handler.getHelpMsg()) + .join('\n'); helpMessage = [ - `/current_issues - мои текущие задачи`, + handlersMessages, `/help` ].join('\n'); } else { - // eslint-disable-next-line prettier/prettier helpMessage = [ `/register `, - `/help` + `/help`, ].join('\n'); } + /* eslint-enable */ this.logger.debug( `Sent help message for telegramChatId = ${msg.chat.id}, ` + `message = ${helpMessage}`, diff --git a/src/telegram-bot/telegram.bot-handler.interface.ts b/src/telegram-bot/telegram.bot-handler.interface.ts new file mode 100644 index 0000000..cc20fc3 --- /dev/null +++ b/src/telegram-bot/telegram.bot-handler.interface.ts @@ -0,0 +1,7 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { TelegramBotService } from './telegram-bot.service'; + +export interface TelegramBotHandlerInterface { + init(service: TelegramBotService, bot: TelegramBot): Promise; + getHelpMsg(): string; +} From a4724069c18b319d1e8b35ae81fbd973489ff7f1 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Wed, 9 Nov 2022 18:46:12 +0700 Subject: [PATCH 02/11] =?UTF-8?q?=D0=98=D1=81=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D1=83=D0=BB=D1=8F=D1=80=D0=BA=D0=B8=20=D0=B8=D0=B7=20=D1=81?= =?UTF-8?q?=D0=B2=D0=BE=D0=B9=D1=81=D1=82=D0=B2=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handlers/current-issues-eccm.bot-handler.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/telegram-bot/handlers/current-issues-eccm.bot-handler.service.ts b/src/telegram-bot/handlers/current-issues-eccm.bot-handler.service.ts index 2392ccc..e4228c0 100644 --- a/src/telegram-bot/handlers/current-issues-eccm.bot-handler.service.ts +++ b/src/telegram-bot/handlers/current-issues-eccm.bot-handler.service.ts @@ -32,7 +32,7 @@ export class CurrentIssuesEccmBotHandlerService if (!this.service) { this.service = service; } - bot.onText(/\/current_issues_eccm/, async (msg) => { + bot.onText(this.forCurrentUser, async (msg) => { const userMetaInfo = await this.userMetaInfoService.findByTelegramId( msg.chat.id, ); From 5140e5a44225543d22b400da96dd84b5bf11eddc Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Fri, 11 Nov 2022 09:21:37 +0700 Subject: [PATCH 03/11] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA=20=D1=87=D1=82=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20rss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/event-emitter/src/rsslistener/rsslistener.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/libs/event-emitter/src/rsslistener/rsslistener.ts b/libs/event-emitter/src/rsslistener/rsslistener.ts index 2d3aa23..3669cdb 100644 --- a/libs/event-emitter/src/rsslistener/rsslistener.ts +++ b/libs/event-emitter/src/rsslistener/rsslistener.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { RssListenerDefaultParams, RssListenerParams, @@ -14,6 +14,8 @@ const parser = new Parser(); @Injectable() export class RssListener implements EventsListener { + private logger = new Logger(RssListener.name); + issues = new BehaviorSubject([]); private updateTimeout; @@ -56,7 +58,15 @@ export class RssListener implements EventsListener { const url = subscription.url; const regexp = new RegExp(subscription.issueNumberParser); const subjectParser = CreateSubjectsParserByRegExp(regexp); - const feed = await parser.parseURL(url); + let feed; + try { + feed = await parser.parseURL(url); + } catch (ex) { + this.logger.error( + `Error at load data from rss by url ${url} with error message ${ex.message}`, + ); + return []; + } const issueNumbers: number[] = feed.items .filter((item) => { const itemDate = new Date(item.pubDate); From d9cfb523b9c873cfd3080e449e8e62380c19b052 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Fri, 11 Nov 2022 09:23:37 +0700 Subject: [PATCH 04/11] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Отправка только последнего уведомления пользователю по задаче * Предотвращены отправки сообщений самому себе --- src/app.module.ts | 16 +---- .../adapters/status-change.adapter.service.ts | 66 +++++++++++++++++++ .../status-change-notifications.service.ts | 2 + 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 35b18cf..46f35c8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -104,20 +104,8 @@ export class AppModule implements OnModuleInit { ); this.personalNotificationAdapterService.send(resp); }); - this.statusChangeNotificationsService.$changes.subscribe((change) => { - const messages = change.messages - .map((m) => m.change_message) - .filter((m) => !!m); - const notifications = change.messages - .map((m) => m.notification_message) - .filter((m) => !!m); - this.logger.log( - `Get status changes messages for ` + - `issue_id = ${change.issue_id}, ` + - `messages = ${JSON.stringify(messages)}, ` + - `notifications = ${JSON.stringify(notifications)}`, - ); - this.statusChangeAdapterService.send(change); + this.statusChangeNotificationsService.$batchChanges.subscribe((changes) => { + this.statusChangeAdapterService.batchSend(changes); }); this.redmineIssuesCacheWriterService.subject diff --git a/src/notifications/adapters/status-change.adapter.service.ts b/src/notifications/adapters/status-change.adapter.service.ts index 06ccba6..dcdba57 100644 --- a/src/notifications/adapters/status-change.adapter.service.ts +++ b/src/notifications/adapters/status-change.adapter.service.ts @@ -1,7 +1,20 @@ import { Injectable } from '@nestjs/common'; +import TelegramBot from 'node-telegram-bot-api'; import { Change } from 'src/models/change.model'; import { TelegramBotService } from 'src/telegram-bot/telegram-bot.service'; +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace StatusChangeAdapter { + export type MsgFromBatch = { + initiatorId: number; + recipientId: number; + issueId: number; + createdAt: number; + msg: string; + options?: TelegramBot.SendMessageOptions; + }; +} + @Injectable() export class StatusChangeAdapterService { constructor(private telegramBotService: TelegramBotService) {} @@ -19,4 +32,57 @@ export class StatusChangeAdapterService { }); return await Promise.all(promises); } + + async batchSend(changes: Change[]): Promise { + const messages = this.getMessages(changes).map((item) => { + item.options = { parse_mode: 'HTML' }; + return item; + }); + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + await this.telegramBotService.sendMessageByRedmineId( + message.recipientId, + message.msg, + message.options, + ); + } + } + + private getMessages(changes: Change[]): StatusChangeAdapter.MsgFromBatch[] { + const res: StatusChangeAdapter.MsgFromBatch[] = []; + const store: Record = {}; + for (let i = 0; i < changes.length; i++) { + const change = changes[i]; + for (let j = 0; j < change.messages.length; j++) { + const message = change.messages[j]; + if (!message.change_message) continue; + if (change.initiator.id == message.recipient.id) continue; + const item: StatusChangeAdapter.MsgFromBatch = { + initiatorId: change.initiator.id, + recipientId: message.recipient.id, + createdAt: change.created_on_timestamp, + issueId: change.issue_id, + msg: message.change_message, + }; + const key = this.keyForMsgFromBatch(item); + if ( + !store[key] || + (store[key] && store[key].createdAt < item.createdAt) + ) { + store[key] = item; + } + } + } + for (const key in store) { + if (Object.prototype.hasOwnProperty.call(store, key)) { + const item = store[key]; + res.push(item); + } + } + return res; + } + + private keyForMsgFromBatch(item: StatusChangeAdapter.MsgFromBatch): string { + return `${item.issueId}-${item.recipientId}`; + } } diff --git a/src/notifications/status-change-notifications.service.ts b/src/notifications/status-change-notifications.service.ts index 23063ea..7c9e525 100644 --- a/src/notifications/status-change-notifications.service.ts +++ b/src/notifications/status-change-notifications.service.ts @@ -19,6 +19,7 @@ export class StatusChangeNotificationsService { private statusChanges: StatusChangesConfig.Config; $changes = new Subject(); + $batchChanges = new Subject(); constructor( private usersService: UsersService, @@ -68,6 +69,7 @@ export class StatusChangeNotificationsService { ); changes.forEach((c) => this.$changes.next(c)); + this.$batchChanges.next(changes); return changes; } From df7e54eb6f23f0277966dbfb327031e686136691 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Fri, 11 Nov 2022 09:50:01 +0700 Subject: [PATCH 05/11] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D1=80=D0=B0=D1=81=D1=81=D1=8B=D0=BB=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20=D1=83=D0=B2?= =?UTF-8?q?=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Отфильтровываются давно созданные уведомления --- configs/main-config.jsonc.dist | 3 ++- src/models/app-config.model.ts | 1 + .../adapters/status-change.adapter.service.ts | 25 ++++++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/configs/main-config.jsonc.dist b/configs/main-config.jsonc.dist index e558201..a42a743 100644 --- a/configs/main-config.jsonc.dist +++ b/configs/main-config.jsonc.dist @@ -6,5 +6,6 @@ } }, "telegramBotToken": "", - "personalMessageTemplate": "" + "personalMessageTemplate": "", + "periodValidityNotification": 43200 // 12h } \ No newline at end of file diff --git a/src/models/app-config.model.ts b/src/models/app-config.model.ts index be968a9..2f04e56 100644 --- a/src/models/app-config.model.ts +++ b/src/models/app-config.model.ts @@ -16,4 +16,5 @@ export type AppConfig = { }; }; telegramBotToken: string; + periodValidityNotification: number; }; diff --git a/src/notifications/adapters/status-change.adapter.service.ts b/src/notifications/adapters/status-change.adapter.service.ts index dcdba57..68ba715 100644 --- a/src/notifications/adapters/status-change.adapter.service.ts +++ b/src/notifications/adapters/status-change.adapter.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import TelegramBot from 'node-telegram-bot-api'; import { Change } from 'src/models/change.model'; import { TelegramBotService } from 'src/telegram-bot/telegram-bot.service'; @@ -17,8 +18,23 @@ namespace StatusChangeAdapter { @Injectable() export class StatusChangeAdapterService { - constructor(private telegramBotService: TelegramBotService) {} + private periodValidityNotification: number; + constructor( + private telegramBotService: TelegramBotService, + private configService: ConfigService, + ) { + this.periodValidityNotification = this.configService.get( + 'periodValidityNotification', + ); + } + + // TODO: Удалить устаревший метод send + + /** + * @deprecated + * @see StatusChangeAdapterService.batchSend + */ async send(change: Change): Promise { const promises = change.messages.map((m) => { if (!m || !m.recipient || !m.recipient.id || !m.notification_message) { @@ -51,8 +67,15 @@ export class StatusChangeAdapterService { private getMessages(changes: Change[]): StatusChangeAdapter.MsgFromBatch[] { const res: StatusChangeAdapter.MsgFromBatch[] = []; const store: Record = {}; + const nowTimestamp = new Date().getTime(); for (let i = 0; i < changes.length; i++) { const change = changes[i]; + if ( + nowTimestamp - change.created_on_timestamp > + this.periodValidityNotification + ) { + continue; + } for (let j = 0; j < change.messages.length; j++) { const message = change.messages[j]; if (!message.change_message) continue; From bba1544733419271ae10e39337ed3cdee68413fe Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Fri, 11 Nov 2022 09:50:46 +0700 Subject: [PATCH 06/11] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=BD=D0=B5=20=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=B2=D1=8B=D0=B1=D1=80=D0=B0=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=D0=BB=D0=B5=20=D1=81=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=BE=D0=B1=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B8=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/notifications/adapters/status-change.adapter.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notifications/adapters/status-change.adapter.service.ts b/src/notifications/adapters/status-change.adapter.service.ts index 68ba715..8fd65da 100644 --- a/src/notifications/adapters/status-change.adapter.service.ts +++ b/src/notifications/adapters/status-change.adapter.service.ts @@ -85,7 +85,7 @@ export class StatusChangeAdapterService { recipientId: message.recipient.id, createdAt: change.created_on_timestamp, issueId: change.issue_id, - msg: message.change_message, + msg: message.notification_message, }; const key = this.keyForMsgFromBatch(item); if ( From edf65c4d7ce8dae08a23922c1f6a2a14a9b10ad2 Mon Sep 17 00:00:00 2001 From: Gnedov Pavel Date: Fri, 11 Nov 2022 15:26:38 +0700 Subject: [PATCH 07/11] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D1=83=D1=81=D0=B0=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87?= =?UTF-8?q?=20=D0=B2=20=D0=B0=D0=BA=D1=82=D0=B8=D0=B2=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8F=D1=85=20=D0=B7=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B8?= =?UTF-8?q?=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- views/daily-eccm-report.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/daily-eccm-report.hbs b/views/daily-eccm-report.hbs index 6b3d305..691ee45 100644 --- a/views/daily-eccm-report.hbs +++ b/views/daily-eccm-report.hbs @@ -36,7 +36,7 @@
    {{#each this.activities}}
  • - {{>redmineIssueAHref issue=this.issue}} (приоритет {{this.issue.priority.name}}; версия {{this.issue.fixed_version.name}}) - {{this.issue.subject}} + {{>redmineIssueAHref issue=this.issue}} (приоритет {{this.issue.priority.name}}; версия {{this.issue.fixed_version.name}}; статус {{this.issue.status.name}}) - {{this.issue.subject}}
      {{#each this.changes}}
    • {{this.created_on}}: {{this.change_message}}
    • From ffc6757b5e8f88fe8482199479549e4377a64852 Mon Sep 17 00:00:00 2001 From: Gnedov Pavel Date: Fri, 11 Nov 2022 16:09:24 +0700 Subject: [PATCH 08/11] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0?= =?UTF-8?q?=20=D1=81=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B7=D0=B0=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/notifications/adapters/status-change.adapter.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notifications/adapters/status-change.adapter.service.ts b/src/notifications/adapters/status-change.adapter.service.ts index 8fd65da..9e9ecaa 100644 --- a/src/notifications/adapters/status-change.adapter.service.ts +++ b/src/notifications/adapters/status-change.adapter.service.ts @@ -78,7 +78,7 @@ export class StatusChangeAdapterService { } for (let j = 0; j < change.messages.length; j++) { const message = change.messages[j]; - if (!message.change_message) continue; + if (!message.notification_message) continue; if (change.initiator.id == message.recipient.id) continue; const item: StatusChangeAdapter.MsgFromBatch = { initiatorId: change.initiator.id, From bfb6c291386b18f4205dca0ee3380fbe593fbafc Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Sat, 12 Nov 2022 20:24:41 +0700 Subject: [PATCH 09/11] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=82=D1=87=D1=91=D1=82=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=B4=D0=B5=D0=B9=D0=BB=D0=B8=20=D0=B8=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/main-config.jsonc.dist | 1 + package-lock.json | 27 ++++ package.json | 2 + src/app.module.ts | 3 + .../daily-eccm-reports.datasource.ts | 41 ++++++ src/models/app-config.model.ts | 1 + src/reports/daily-eccm.report.controller.ts | 61 +++++++-- src/reports/daily-eccm.report.service.ts | 128 +++++++++++++++--- views/daily-eccm-report.hbs | 4 +- 9 files changed, 234 insertions(+), 34 deletions(-) create mode 100644 src/couchdb-datasources/daily-eccm-reports.datasource.ts diff --git a/configs/main-config.jsonc.dist b/configs/main-config.jsonc.dist index a42a743..9e26c4d 100644 --- a/configs/main-config.jsonc.dist +++ b/configs/main-config.jsonc.dist @@ -3,6 +3,7 @@ "dbs": { "changes": "", "userMetaInfo": "", + "eccmDailyReports": "" } }, "telegramBotToken": "", diff --git a/package-lock.json b/package-lock.json index bc367dd..6e0d453 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "handlebars": "^4.7.7", "hbs": "^4.2.0", "imap-simple": "^5.1.0", + "luxon": "^3.1.0", "nano": "^10.0.0", "node-telegram-bot-api": "^0.59.0", "reflect-metadata": "^0.1.13", @@ -36,6 +37,7 @@ "@types/cache-manager": "^4.0.1", "@types/express": "^4.17.13", "@types/jest": "27.4.0", + "@types/luxon": "^3.1.0", "@types/node": "^16.0.0", "@types/node-telegram-bot-api": "^0.57.1", "@types/supertest": "^2.0.11", @@ -2088,6 +2090,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/luxon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.1.0.tgz", + "integrity": "sha512-gCd/HcCgjqSxfMrgtqxCgYk/22NBQfypwFUG7ZAyG/4pqs51WLTcUzVp1hqTbieDYeHS3WoVEh2Yv/2l+7B0Vg==", + "dev": true + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -7195,6 +7203,14 @@ "node": ">=12" } }, + "node_modules/luxon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.0.tgz", + "integrity": "sha512-7w6hmKC0/aoWnEsmPCu5Br54BmbmUp5GfcqBxQngRcXJ+q5fdfjEzn7dxmJh2YdDhgW8PccYtlWKSv4tQkrTQg==", + "engines": { + "node": ">=12" + } + }, "node_modules/macos-release": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", @@ -11901,6 +11917,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "@types/luxon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.1.0.tgz", + "integrity": "sha512-gCd/HcCgjqSxfMrgtqxCgYk/22NBQfypwFUG7ZAyG/4pqs51WLTcUzVp1hqTbieDYeHS3WoVEh2Yv/2l+7B0Vg==", + "dev": true + }, "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -15760,6 +15782,11 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.2.tgz", "integrity": "sha512-VJL3nIpA79TodY/ctmZEfhASgqekbT574/c4j3jn4bKXbSCnTTCH/KltZyvL2GlV+tGSMtsWyem8DCX7qKTMBA==" }, + "luxon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.0.tgz", + "integrity": "sha512-7w6hmKC0/aoWnEsmPCu5Br54BmbmUp5GfcqBxQngRcXJ+q5fdfjEzn7dxmJh2YdDhgW8PccYtlWKSv4tQkrTQg==" + }, "macos-release": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", diff --git a/package.json b/package.json index 4bfddfc..54839ed 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "handlebars": "^4.7.7", "hbs": "^4.2.0", "imap-simple": "^5.1.0", + "luxon": "^3.1.0", "nano": "^10.0.0", "node-telegram-bot-api": "^0.59.0", "reflect-metadata": "^0.1.13", @@ -48,6 +49,7 @@ "@types/cache-manager": "^4.0.1", "@types/express": "^4.17.13", "@types/jest": "27.4.0", + "@types/luxon": "^3.1.0", "@types/node": "^16.0.0", "@types/node-telegram-bot-api": "^0.57.1", "@types/supertest": "^2.0.11", diff --git a/src/app.module.ts b/src/app.module.ts index 46f35c8..0b6f99b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -29,6 +29,7 @@ import { CurrentIssuesEccmReportController } from './reports/current-issues-eccm import { DailyEccmReportController } from './reports/daily-eccm.report.controller'; import { DailyEccmReportService } from './reports/daily-eccm.report.service'; import { ChangesService } from './changes/changes.service'; +import { DailyEccmReportsDatasource } from './couchdb-datasources/daily-eccm-reports.datasource'; @Module({ imports: [ @@ -64,6 +65,7 @@ import { ChangesService } from './changes/changes.service'; CurrentIssuesEccmBotHandlerService, DailyEccmReportService, ChangesService, + DailyEccmReportsDatasource, ], }) export class AppModule implements OnModuleInit { @@ -88,6 +90,7 @@ export class AppModule implements OnModuleInit { Users.getDatasource(); Changes.getDatasource(); UserMetaInfo.getDatasource(); + DailyEccmReportsDatasource.getDatasource(); this.enhancerService.addEnhancer([ this.timestampEnhancer, diff --git a/src/couchdb-datasources/daily-eccm-reports.datasource.ts b/src/couchdb-datasources/daily-eccm-reports.datasource.ts new file mode 100644 index 0000000..8ff3f22 --- /dev/null +++ b/src/couchdb-datasources/daily-eccm-reports.datasource.ts @@ -0,0 +1,41 @@ +import { CouchDb } from '@app/event-emitter/couchdb-datasources/couchdb'; +import { Injectable, Logger } from '@nestjs/common'; +import nano from 'nano'; +import { DailyEccmReport } from 'src/reports/daily-eccm.report.service'; +import configuration from '../configs/app'; + +const config = configuration(); + +@Injectable() +export class DailyEccmReportsDatasource { + private static logger = new Logger(DailyEccmReportsDatasource.name); + private static db = null; + private static initilized = false; + + static async getDatasource(): Promise< + nano.DocumentScope + > { + if (DailyEccmReportsDatasource.initilized) { + return DailyEccmReportsDatasource.db; + } + DailyEccmReportsDatasource.initilized = true; + const n = CouchDb.getCouchDb(); + const dbName = config.couchDb.dbs.eccmDailyReports; + const dbs = await n.db.list(); + if (!dbs.includes(dbName)) { + await n.db.create(dbName); + } + DailyEccmReportsDatasource.db = await n.db.use(dbName); + DailyEccmReportsDatasource.initilized = true; + DailyEccmReportsDatasource.logger.log( + `Connected to eccm-daily-reports db - ${dbName}`, + ); + return DailyEccmReportsDatasource.db; + } + + async getDatasource(): Promise< + nano.DocumentScope + > { + return await DailyEccmReportsDatasource.getDatasource(); + } +} diff --git a/src/models/app-config.model.ts b/src/models/app-config.model.ts index 2f04e56..b1352af 100644 --- a/src/models/app-config.model.ts +++ b/src/models/app-config.model.ts @@ -13,6 +13,7 @@ export type AppConfig = { dbs: { changes: string; userMetaInfo: string; + eccmDailyReports: string; }; }; telegramBotToken: string; diff --git a/src/reports/daily-eccm.report.controller.ts b/src/reports/daily-eccm.report.controller.ts index 8721885..3c9f971 100644 --- a/src/reports/daily-eccm.report.controller.ts +++ b/src/reports/daily-eccm.report.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Query, Render } from '@nestjs/common'; +import { Controller, Get, Param, Query, Render } from '@nestjs/common'; import { DailyEccmReport, DailyEccmReportService, @@ -10,28 +10,61 @@ export class DailyEccmReportController { @Get() @Render('daily-eccm-report') + async getReportDefault( + @Query('from') from: string, + @Query('to') to: string, + ): Promise { + return await this.getReport(from, to); + } + + @Get('/delete/:name') + async deleteReport(@Param('name') name: string): Promise { + return await this.dailyEccmReportService.deleteReport(name); + } + + @Get('/load/:name') + @Render('daily-eccm-report') + async loadReport( + @Param('name') name: string, + ): Promise { + return await this.dailyEccmReportService.loadReport(name); + } + + @Get('/load/:name/raw') + async loadReportRawData( + @Param('name') name: string, + ): Promise { + return await this.dailyEccmReportService.loadReport(name); + } + + @Get('/generate') + @Render('daily-eccm-report') async getReport( @Query('from') from: string, @Query('to') to: string, + @Query('name') name?: string, + @Query('overwrite') overwrite?: boolean, ): Promise { - const now = new Date().toISOString(); - return await this.dailyEccmReportService.generateReport({ - from: from, - to: to, - reportDate: now, - }); + return await this.getReportRawData(from, to, name, overwrite); } - @Get('/raw') + @Get('/generate/raw') async getReportRawData( @Query('from') from: string, @Query('to') to: string, + @Query('name') name?: string, + @Query('overwrite') overwrite?: boolean, ): Promise { - const now = new Date().toISOString(); - return await this.dailyEccmReportService.generateReport({ - from: from, - to: to, - reportDate: now, - }); + const params = this.dailyEccmReportService.generateParams(from, to, name); + const data = await this.dailyEccmReportService.generateReport(params); + if (name) { + const saveResult = await this.dailyEccmReportService.saveReport( + data, + overwrite, + ); + return saveResult ? data : null; + } else { + return data; + } } } diff --git a/src/reports/daily-eccm.report.service.ts b/src/reports/daily-eccm.report.service.ts index f249cd8..45010e6 100644 --- a/src/reports/daily-eccm.report.service.ts +++ b/src/reports/daily-eccm.report.service.ts @@ -10,6 +10,10 @@ import { TimestampConverter } from '@app/event-emitter/utils/timestamp-converter import { IssuesService } from '@app/event-emitter/issues/issues.service'; import { UsersService } from '@app/event-emitter/users/users.service'; import { GetParentsHint } from '@app/event-emitter/utils/get-parents-hint'; +import { DailyEccmReportsDatasource } from 'src/couchdb-datasources/daily-eccm-reports.datasource'; +import { Timestamped } from '@app/event-emitter/models/timestamped'; +import { TimestampNowFill } from '@app/event-emitter/utils/timestamp-now-fill'; +import { DateTime } from 'luxon'; // eslint-disable-next-line @typescript-eslint/no-namespace export namespace DailyEccmReport { @@ -96,7 +100,9 @@ export namespace DailyEccmReport { export type Params = { from: string; to: string; - reportDate: string; + name: string; + project: string; + versions: string[]; }; export type IssueAndChanges = { @@ -140,28 +146,32 @@ export namespace DailyEccmReport { private eccmUsers: RedmineTypes.User[], private issuesService: IssuesService, ) { - const users: Models.UserReport[] = this.eccmUsers.map((user) => { - if (!user) { - this.logger.error(`Not found user data`); - return; - } - const u: Models.User = { - id: user.id, - lastname: user.lastname, - firstname: user.firstname, - }; - return { - user: u, - activities: [], - issuesGroupedByStatus: [], - } as Models.UserReport; - }).filter((user) => Boolean(user)); + const users: Models.UserReport[] = this.eccmUsers + .map((user) => { + if (!user) { + this.logger.error(`Not found user data`); + return; + } + const u: Models.User = { + id: user.id, + lastname: user.lastname, + firstname: user.firstname, + }; + return { + user: u, + activities: [], + issuesGroupedByStatus: [], + } as Models.UserReport; + }) + .filter((user) => Boolean(user)); this.report = { byUsers: users, params: { from: '', to: '', - reportDate: '', + name: '', + project: '', + versions: [], }, }; } @@ -173,7 +183,9 @@ export namespace DailyEccmReport { setParams(params: Models.Params): void { this.report.params.from = params.from; this.report.params.to = params.to; - this.report.params.reportDate = params.reportDate; + this.report.params.name = params.name; + this.report.params.versions = [...params.versions]; + this.report.params.project = params.project; } async setCurrentIssues(issues: RedmineTypes.Issue[]): Promise { @@ -355,6 +367,7 @@ export class DailyEccmReportService { private changesService: ChangesService, private issuesService: IssuesService, private usersService: UsersService, + private dailyEccmReportsDatasource: DailyEccmReportsDatasource, ) { this.eccmConfig = this.configService.get('redmineEccm'); } @@ -389,6 +402,83 @@ export class DailyEccmReportService { return report.get(); } + generateParams( + from: string, + to: string, + name?: string, + ): DailyEccmReport.Models.Params { + const fromDate = DateTime.fromISO(from); + if (!fromDate.isValid) throw new Error('from is invalid date'); + const toDate = DateTime.fromISO(to); + if (!toDate.isValid) throw new Error('to is invalid date'); + let nameValue: string | null = name || null; + if (!nameValue) { + nameValue = DateTime.now().toFormat('yyyy-MM-dd'); + } + return { + from: fromDate.toISO(), + to: toDate.toISO(), + name: nameValue, + project: this.eccmConfig.projectName, + versions: [...this.eccmConfig.currentVersions], + }; + } + + async saveReport( + report: DailyEccmReport.Models.Report, + overwrite?: boolean, + ): Promise { + const name = report.params.name; + const existsReport = await this.loadReport(name); + if (existsReport && !overwrite) { + return false; + } + + type ReportType = nano.DocumentGetResponse & + DailyEccmReport.Models.Report & + Timestamped; + + const newReport: ReportType = TimestampNowFill({ + ...report, + _id: name, + _rev: existsReport?._rev, + }); + + const datasource = await this.dailyEccmReportsDatasource.getDatasource(); + await datasource.insert(newReport); + + return true; + } + + async loadReport( + name: string, + ): Promise< + | (DailyEccmReport.Models.Report & nano.DocumentGetResponse & Timestamped) + | null + > { + const datasource = await this.dailyEccmReportsDatasource.getDatasource(); + let resp: any = null; + try { + resp = await datasource.get(name); + } catch (ex) { + this.logger.warn( + `Cannot load report ${name} with error message ${ex.message}`, + ); + } + return resp; + } + + async deleteReport(name: string): Promise { + const datasource = await this.dailyEccmReportsDatasource.getDatasource(); + const report = await this.loadReport(name); + let res = false; + try { + await datasource.destroy(report._id, report._rev); + res = true; + } catch (ex) {} + return res; + } + private async createEmptyReport(): Promise { const usersList: string[] = this.eccmConfig.groups.reduce((acc, group) => { group.people.forEach((fullname) => { diff --git a/views/daily-eccm-report.hbs b/views/daily-eccm-report.hbs index 691ee45..02c752f 100644 --- a/views/daily-eccm-report.hbs +++ b/views/daily-eccm-report.hbs @@ -10,7 +10,9 @@
      • От - {{this.params.from}}
      • До - {{this.params.to}}
      • -
      • Дата выгрузки - {{this.params.reportDate}}
      • +
      • Имя отчёта - {{this.params.name}}
      • +
      • Имя проекта - {{this.params.project}}
      • +
      • Версии - {{this.params.versions}}

      Отчёт по работникам

      From 4452a3fa2b9847b309da7060d898f371e8430c68 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Sat, 12 Nov 2022 20:26:09 +0700 Subject: [PATCH 10/11] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BD=20=D0=BB?= =?UTF-8?q?=D0=B8=D1=88=D0=BD=D0=B8=D0=B9=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.module.ts b/src/app.module.ts index 0b6f99b..d962a37 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,7 +4,7 @@ import { EnhancerService } from '@app/event-emitter/issue-enhancers/enhancer.ser import { TimestampEnhancer } from '@app/event-emitter/issue-enhancers/timestamps-enhancer'; import { MainController } from '@app/event-emitter/main/main.controller'; import { CacheModule, Logger, Module, OnModuleInit } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ConfigModule } from '@nestjs/config'; import { switchMap, tap } from 'rxjs'; import { AppController } from './app.controller'; import { AppService } from './app.service'; From cc34a620f1f2bf2fe89a6c4492626e443c09fe05 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Mon, 14 Nov 2022 19:19:59 +0700 Subject: [PATCH 11/11] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BE=D1=82=D1=87=D1=91=D1=82=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B4=D0=B5=D0=B9=D0=BB=D0=B8=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D1=80=D0=B0=D1=81=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/eccm-config.jsonc.dist | 6 ++- package-lock.json | 76 +++++++++++++++++++++++++++ package.json | 2 + src/app.module.ts | 4 ++ src/models/eccm-config.model.ts | 4 ++ src/reports/daily-eccm.report.task.ts | 67 +++++++++++++++++++++++ 6 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/reports/daily-eccm.report.task.ts diff --git a/configs/eccm-config.jsonc.dist b/configs/eccm-config.jsonc.dist index 3975be1..729d9f0 100644 --- a/configs/eccm-config.jsonc.dist +++ b/configs/eccm-config.jsonc.dist @@ -7,5 +7,9 @@ "name": "", "people": [""] } - ] + ], + "dailyTime": { + "hour": 9, + "minute": 0 + } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6e0d453..63fd4b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@nestjs/core": "^8.0.0", "@nestjs/platform-express": "^8.0.0", "@nestjs/platform-socket.io": "^8.4.4", + "@nestjs/schedule": "^2.1.0", "@nestjs/serve-static": "^2.2.2", "@nestjs/websockets": "^8.4.4", "axios": "^0.27.2", @@ -35,6 +36,7 @@ "@nestjs/schematics": "^8.0.0", "@nestjs/testing": "^8.0.0", "@types/cache-manager": "^4.0.1", + "@types/cron": "^2.0.0", "@types/express": "^4.17.13", "@types/jest": "27.4.0", "@types/luxon": "^3.1.0", @@ -1630,6 +1632,20 @@ "rxjs": "^7.1.0" } }, + "node_modules/@nestjs/schedule": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-2.1.0.tgz", + "integrity": "sha512-4Xaw56WiW3VsxEPPnj/iDtfjcO+sUZyYAeRxD0gnF5havncxjAnv52Iw7UH3DuzzUA784xPGgGje3Fq0Gu925g==", + "dependencies": { + "cron": "2.0.0", + "uuid": "8.3.2" + }, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0", + "reflect-metadata": "^0.1.12" + } + }, "node_modules/@nestjs/schematics": { "version": "8.0.11", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-8.0.11.tgz", @@ -1986,6 +2002,16 @@ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" }, + "node_modules/@types/cron": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/cron/-/cron-2.0.0.tgz", + "integrity": "sha512-xZM08fqvwIXgghtPVkSPKNgC+JoMQ2OHazEvyTKnNf7aWu1aB6/4lBbQFrb03Td2cUGG7ITzMv3mFYnMu6xRaQ==", + "dev": true, + "dependencies": { + "@types/luxon": "*", + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "8.4.5", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz", @@ -3640,6 +3666,22 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cron": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-2.0.0.tgz", + "integrity": "sha512-RPeRunBCFr/WEo7WLp8Jnm45F/ziGJiHVvVQEBSDTSGu6uHW49b2FOP2O14DcXlGJRLhwE7TIoDzHHK4KmlL6g==", + "dependencies": { + "luxon": "^1.23.x" + } + }, + "node_modules/cron/node_modules/luxon": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz", + "integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==", + "engines": { + "node": "*" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -11534,6 +11576,15 @@ "tslib": "2.4.0" } }, + "@nestjs/schedule": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-2.1.0.tgz", + "integrity": "sha512-4Xaw56WiW3VsxEPPnj/iDtfjcO+sUZyYAeRxD0gnF5havncxjAnv52Iw7UH3DuzzUA784xPGgGje3Fq0Gu925g==", + "requires": { + "cron": "2.0.0", + "uuid": "8.3.2" + } + }, "@nestjs/schematics": { "version": "8.0.11", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-8.0.11.tgz", @@ -11813,6 +11864,16 @@ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" }, + "@types/cron": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/cron/-/cron-2.0.0.tgz", + "integrity": "sha512-xZM08fqvwIXgghtPVkSPKNgC+JoMQ2OHazEvyTKnNf7aWu1aB6/4lBbQFrb03Td2cUGG7ITzMv3mFYnMu6xRaQ==", + "dev": true, + "requires": { + "@types/luxon": "*", + "@types/node": "*" + } + }, "@types/eslint": { "version": "8.4.5", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz", @@ -13124,6 +13185,21 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "cron": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-2.0.0.tgz", + "integrity": "sha512-RPeRunBCFr/WEo7WLp8Jnm45F/ziGJiHVvVQEBSDTSGu6uHW49b2FOP2O14DcXlGJRLhwE7TIoDzHHK4KmlL6g==", + "requires": { + "luxon": "^1.23.x" + }, + "dependencies": { + "luxon": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz", + "integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==" + } + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", diff --git a/package.json b/package.json index 54839ed..c2455af 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@nestjs/core": "^8.0.0", "@nestjs/platform-express": "^8.0.0", "@nestjs/platform-socket.io": "^8.4.4", + "@nestjs/schedule": "^2.1.0", "@nestjs/serve-static": "^2.2.2", "@nestjs/websockets": "^8.4.4", "axios": "^0.27.2", @@ -47,6 +48,7 @@ "@nestjs/schematics": "^8.0.0", "@nestjs/testing": "^8.0.0", "@types/cache-manager": "^4.0.1", + "@types/cron": "^2.0.0", "@types/express": "^4.17.13", "@types/jest": "27.4.0", "@types/luxon": "^3.1.0", diff --git a/src/app.module.ts b/src/app.module.ts index d962a37..34a48ae 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -30,6 +30,8 @@ import { DailyEccmReportController } from './reports/daily-eccm.report.controlle import { DailyEccmReportService } from './reports/daily-eccm.report.service'; import { ChangesService } from './changes/changes.service'; import { DailyEccmReportsDatasource } from './couchdb-datasources/daily-eccm-reports.datasource'; +import { ScheduleModule } from '@nestjs/schedule'; +import { DailyEccmReportTask } from './reports/daily-eccm.report.task'; @Module({ imports: [ @@ -40,6 +42,7 @@ import { DailyEccmReportsDatasource } from './couchdb-datasources/daily-eccm-rep CacheModule.register({ isGlobal: true, }), + ScheduleModule.forRoot(), ], controllers: [ AppController, @@ -66,6 +69,7 @@ import { DailyEccmReportsDatasource } from './couchdb-datasources/daily-eccm-rep DailyEccmReportService, ChangesService, DailyEccmReportsDatasource, + DailyEccmReportTask, ], }) export class AppModule implements OnModuleInit { diff --git a/src/models/eccm-config.model.ts b/src/models/eccm-config.model.ts index 37317b7..74b3ab9 100644 --- a/src/models/eccm-config.model.ts +++ b/src/models/eccm-config.model.ts @@ -10,5 +10,9 @@ export namespace EccmConfig { projectName: string; currentIssuesStatuses: string[]; groups: UserGroup[]; + dailyTime: { + hour: number; + minute: number; + }; }; } diff --git a/src/reports/daily-eccm.report.task.ts b/src/reports/daily-eccm.report.task.ts new file mode 100644 index 0000000..b986936 --- /dev/null +++ b/src/reports/daily-eccm.report.task.ts @@ -0,0 +1,67 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import { DailyEccmReportService } from './daily-eccm.report.service'; +import { DateTime, Duration } from 'luxon'; +import { ConfigService } from '@nestjs/config'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace Models { + export type Time = { + hour: number; + minute: number; + }; +} + +@Injectable() +export class DailyEccmReportTask { + private logger = new Logger(DailyEccmReportTask.name); + private eccmDailyTime: Models.Time; + + constructor( + private dailyEccmReportService: DailyEccmReportService, + private configService: ConfigService, + ) { + this.eccmDailyTime = this.configService.get( + 'redmineEccm.dailyTime', + ); + } + + @Cron('25 9,10 1-5 * *') + async generateReport(): Promise { + this.logger.log(`Generate daily eccm report by cron task started`); + const now = DateTime.now(); + const toDate = DateTime.local( + now.year, + now.month, + now.day, + this.eccmDailyTime.hour, + this.eccmDailyTime.minute, + ); + let duration: Duration; + if (now.weekday == 1) { + duration = Duration.fromObject({ days: -3 }); + } else { + duration = Duration.fromObject({ days: -1 }); + } + const fromDate = toDate.plus(duration); + const name = now.toFormat('yyyy-MM-dd'); + const params = this.dailyEccmReportService.generateParams( + fromDate.toJSDate().toISOString(), + toDate.toJSDate().toISOString(), + name, + ); + + const reportData = await this.dailyEccmReportService.generateReport(params); + const saveResult = await this.dailyEccmReportService.saveReport( + reportData, + true, + ); + this.logger.log( + `Generate daily eccm report by cron task ` + + `finished with params = ${JSON.stringify(params)} ` + + `and result = ${saveResult}`, + ); + + return; + } +}