From ebd630b25a0e4b42613a77bc0846c30c4b29193d Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Fri, 14 Oct 2022 11:00:50 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B2=D1=8B=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=83=20?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=83=D1=89=D0=B8=D1=85=20=D0=B7=D0=B0=D0=B4?= =?UTF-8?q?=D0=B0=D1=87=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D0=B0=20eccm=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20te?= =?UTF-8?q?legram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 4 + .../current-issues-eccm.report.service.ts | 223 ++++++++++++++++++ .../current-issues.bot-handler.service.ts | 46 ++++ src/telegram-bot/telegram-bot.service.ts | 3 + 4 files changed, 276 insertions(+) create mode 100644 src/reports/current-issues-eccm.report.service.ts create mode 100644 src/telegram-bot/handlers/current-issues.bot-handler.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 040bf33..d696e15 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -23,6 +23,8 @@ import { UserMetaInfoService } from './user-meta-info/user-meta-info.service'; 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'; @Module({ imports: [ @@ -49,6 +51,8 @@ import { StatusChangeAdapterService } from './notifications/adapters/status-chan UserMetaInfo, PersonalNotificationAdapterService, StatusChangeAdapterService, + CurrentIssuesEccmReportService, + CurrentIssuesBotHandlerService, ], }) export class AppModule implements OnModuleInit { diff --git a/src/reports/current-issues-eccm.report.service.ts b/src/reports/current-issues-eccm.report.service.ts new file mode 100644 index 0000000..d357db4 --- /dev/null +++ b/src/reports/current-issues-eccm.report.service.ts @@ -0,0 +1,223 @@ +/* eslint-disable prettier/prettier */ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Issues } from '@app/event-emitter/couchdb-datasources/issues'; +import { UNLIMITED } from '@app/event-emitter/consts/consts'; +import { RedmineTypes } from '@app/event-emitter/models/redmine-types'; +import nano from 'nano'; +import Handlebars from 'handlebars'; +import { RedminePublicUrlConverter } from 'src/converters/redmine-public-url.converter'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace CurrentIssuesEccmReport { + export type Options = { + statuses?: string[]; + versions?: string[]; + userIds?: number[]; + project?: string; + }; + + // eslint-disable-next-line @typescript-eslint/no-namespace + export namespace Defaults { + export const statuses = [ + 'In Progress', + 'Feedback', + 'Re-opened', + 'Code Review', + 'Resolved', + 'Testing', + ]; + + export const projectName = 'ECCM'; + } + + export type User = { + id: number; + firstname: string; + lastname: string; + }; + + export type Status = { + id: number; + name: string; + }; + + export type IssuesAndStatus = { + status: Status; + issues: RedmineTypes.Issue[]; + }; + + export type UserReport = { + user: User; + issuesGroupedByStatus: IssuesAndStatus[]; + }; + + export class UsersReport { + private report: UserReport[] = []; + + private reportFormatter: HandlebarsTemplateDelegate; + private reportTemplate = [ + // context - this.report: UserReport[] + '{{#each this}}', + // context - UserReport + '{{user.firstname}} {{user.lastname}}:', + '', + '{{#each issuesGroupedByStatus}}', + // context - IssuesAndStatus + '{{status.name}}:', + '{{#each issues}}', + // context - RedmineTypes.Issue + ` - {{>redmineIssueAHref issue=.}}: {{subject}} (прио - {{priority.name}}, версия - {{fixed_version.name}})`, + '{{/each}}', + '{{/each}}', + '', + '{{/each}}', + ].join('\n'); + + constructor( + private redminePublicUrlConverter: RedminePublicUrlConverter, + private redminePublicUrl: string, + ) { + this.reportFormatter = Handlebars.compile(this.reportTemplate); + Handlebars.registerPartial( + 'redmineIssueAHref', + `{{issue.tracker.name}} #{{issue.id}}`, + ); + } + + push(item: any): void { + const user: User = { + id: item.current_user.id, + firstname: item.current_user.firstname, + lastname: item.current_user.lastname, + }; + const status: Status = { + id: item.status.id, + name: item.status.name, + }; + const issuesAndStatus = this.getOrCreateIssuesAndStatus(user, status); + issuesAndStatus.issues.push(item); + } + + getOrCreateUserReport(user: User): UserReport { + let userReport: UserReport; + userReport = this.report.find((r) => r.user.id == user.id); + if (!userReport) { + userReport = { + user: user, + issuesGroupedByStatus: [], + }; + this.report.push(userReport); + } + return userReport; + } + + getOrCreateIssuesAndStatus(user: User, status: Status): IssuesAndStatus { + const userReport = this.getOrCreateUserReport(user); + let issuesAndStatus: IssuesAndStatus; + issuesAndStatus = userReport.issuesGroupedByStatus.find( + (i) => i.status.id == status.id, + ); + if (!issuesAndStatus) { + issuesAndStatus = { + issues: [], + status: status, + }; + userReport.issuesGroupedByStatus.push(issuesAndStatus); + } + return issuesAndStatus; + } + + getAllUsersReportByTemplate(): string { + return this.reportFormatter(this.report); + } + } +} + +@Injectable() +export class CurrentIssuesEccmReportService { + private eccmVersions: string[]; + private logger = new Logger(CurrentIssuesEccmReportService.name); + + constructor( + private configService: ConfigService, + private issuesDatasource: Issues, + private redminePublicUrlConverter: RedminePublicUrlConverter, + ) { + this.eccmVersions = this.configService.get('redmineEccmVersions'); + } + + async getData( + options?: CurrentIssuesEccmReport.Options, + ): Promise { + const datasource = await this.issuesDatasource.getDatasource(); + + const query: nano.MangoQuery = { + selector: { + 'status.name': { + $in: CurrentIssuesEccmReport.Defaults.statuses, + }, + 'fixed_version.name': { + $in: this.eccmVersions, + }, + 'project.name': + options?.project || CurrentIssuesEccmReport.Defaults.projectName, + }, + fields: [ + 'id', + 'tracker.name', + 'status.id', + 'status.name', + 'priority.id', + 'priority.name', + 'fixed_version.name', + 'subject', + 'updated_on', + 'updated_on_timestamp', + 'current_user.id', + 'current_user.firstname', + 'current_user.lastname', + ], + limit: UNLIMITED, + }; + + if (options && options.statuses) { + query.selector['status.name'] = { + $in: options.statuses, + }; + } + if (options && options.versions) { + query.selector['fixed_version.name'] = { + $in: options.versions, + }; + } + if (options && options.userIds) { + query.selector['current_user.id'] = { + $in: options.userIds, + }; + } else { + query.selector['current_user.id'] = { + $exists: true, + }; + } + + this.logger.debug(`Query for get report data: ${JSON.stringify(query)}`); + + const rawData = await datasource.find(query); + this.logger.debug(`Raw data for report: ${JSON.stringify(rawData)}`); + const data = rawData.docs as RedmineTypes.Issue[]; + return data; + } + + async getReport(options?: CurrentIssuesEccmReport.Options): Promise { + this.logger.debug(`Options for report: ${JSON.stringify(options || null)}`); + const data = await this.getData(options); + this.logger.debug(`Data for report ${JSON.stringify(data)}`); + const report = new CurrentIssuesEccmReport.UsersReport( + this.redminePublicUrlConverter, + this.configService.get('redmineUrlPublic'), + ); + data.forEach((item) => report.push(item)); + return report.getAllUsersReportByTemplate(); + } +} diff --git a/src/telegram-bot/handlers/current-issues.bot-handler.service.ts b/src/telegram-bot/handlers/current-issues.bot-handler.service.ts new file mode 100644 index 0000000..0e047e2 --- /dev/null +++ b/src/telegram-bot/handlers/current-issues.bot-handler.service.ts @@ -0,0 +1,46 @@ +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 { 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'; + +@Injectable() +export class CurrentIssuesBotHandlerService { + private forName = /\/current_issues_eccm (.+) (.+)/; + private forCurrentUser = /\/current_issues_eccm/; + private service: TelegramBotService; + private eccmConfig: EccmConfig.Config; + private logger = new Logger(CurrentIssuesBotHandlerService.name); + + constructor( + private configService: ConfigService, + private currentIssuesEccmReportService: CurrentIssuesEccmReportService, + private userMetaInfoService: UserMetaInfoService, + ) { + this.eccmConfig = this.configService.get('redmineEccm'); + } + + async init(service: TelegramBotService, bot: TelegramBot): Promise { + if (!this.service) { + this.service = service; + } + bot.onText(/\/current_issues_eccm/, async (msg) => { + const userMetaInfo = await this.userMetaInfoService.findByTelegramId( + msg.chat.id, + ); + const redmineUserId = userMetaInfo.user_id; + const report = await this.currentIssuesEccmReportService.getReport({ + userIds: [redmineUserId], + project: this.eccmConfig.projectName, + versions: this.eccmConfig.currentVersions, + statuses: this.eccmConfig.currentIssuesStatuses, + }); + this.logger.debug(`Current issues eccm report: ${report}`); + bot.sendMessage(msg.chat.id, report || 'empty report', { + parse_mode: 'HTML', + }); + }); + } +} diff --git a/src/telegram-bot/telegram-bot.service.ts b/src/telegram-bot/telegram-bot.service.ts index 9e28369..37da5fa 100644 --- a/src/telegram-bot/telegram-bot.service.ts +++ b/src/telegram-bot/telegram-bot.service.ts @@ -5,6 +5,7 @@ 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'; @Injectable() export class TelegramBotService { @@ -19,6 +20,7 @@ export class TelegramBotService { private userMetaInfoService: UserMetaInfoService, private usersService: UsersService, private configService: ConfigService, + private currentIssuesBotHandlerService: CurrentIssuesBotHandlerService, ) { this.telegramBotToken = this.configService.get('telegramBotToken'); this.redminePublicUrlPrefix = @@ -41,6 +43,7 @@ export class TelegramBotService { this.bot.onText(/\/leave/, async (msg) => { await this.leave(msg); }); + this.currentIssuesBotHandlerService.init(this, this.bot); } private async showHelpMessage(msg: TelegramBot.Message): Promise {