import { IssueEnhancerInterface } from '@app/event-emitter/issue-enhancers/issue-enhancer-interface'; import { RedmineTypes } from '@app/event-emitter/models/redmine-types'; import { Injectable, Logger } from '@nestjs/common'; import * as Luxon from 'luxon'; import { CalendarEvent } from '../models/calendar-event'; import * as DatesParser from '../utils/string-with-dates-parser'; export type DescriptionParserParams = { title: string; lineRegexp: string; }; export type CustomFieldParserParams = { dateFormat: string; customFieldName: string; /** Время в минутах */ interval?: number; fullDay?: boolean; alias?: string; }; export const UNKNOWN_CALENDAR_EVENT = 'Unknown calendar event'; @Injectable() export class CalendarEnhancer implements IssueEnhancerInterface { private logger = new Logger(CalendarEnhancer.name); name = 'calendar'; constructor( public useForProjects: string[], public customFields: CustomFieldParserParams[], public descriptionCalendarParams: DescriptionParserParams, public calendarEventsKey: string, ) { const initParams = { useForProjects, customFields, descriptionCalendarParams, calendarEventsKey, }; this.logger.debug( `Calendar enhancer init with ${JSON.stringify(initParams)}`, ); } async enhance( issue: RedmineTypes.ExtendedIssue, ): Promise { const res: RedmineTypes.ExtendedIssue = { ...issue }; if (!this.checkProject(res)) { return res; } try { res[this.calendarEventsKey] = this.getCalendarEvents(res); } catch (ex) { this.logger.error( `Error at parsing calendar events, message - ${ex}: ${ (ex as Error)?.stack }`, ); return res; } this.logger.debug( `Calendar events for #${issue.id}: issue.${ this.calendarEventsKey } = ${JSON.stringify(res[this.calendarEventsKey])}`, ); return res; } private checkProject(issue: RedmineTypes.ExtendedIssue): boolean { if (this.useForProjects.indexOf('*') >= 0) { return true; } if ( this.useForProjects.length > 0 && this.useForProjects.indexOf(issue.project.name) >= 0 ) { return true; } return false; } private getCalendarEvents( issue: RedmineTypes.ExtendedIssue, ): CalendarEvent[] { return [ ...this.getCalendarEventsFromCustomFields(issue), ...this.getCalendarEventsFromSubject(issue), ...this.getCalendarEventsFromDescription(issue), ]; } private getCalendarEventsFromCustomFields( issue: RedmineTypes.ExtendedIssue, ): CalendarEvent[] { const res: CalendarEvent[] = []; for (let i = 0; i < this.customFields.length; i++) { const cfParam = this.customFields[i]; const event = this.getCalendarEventFromCustomField(issue, cfParam); if (event) res.push(event); } return res; } private getCalendarEventFromCustomField( issue: RedmineTypes.ExtendedIssue, params: CustomFieldParserParams, ): CalendarEvent | null { if (typeof params.interval !== 'number' && params.fullDay !== true) return null; const cf = issue.custom_fields.find((issueCf) => { return issueCf.name === params.customFieldName; }); if (!cf) return null; const from = Luxon.DateTime.fromFormat(cf.value, params.dateFormat); if (!from.isValid) return null; let to: Luxon.DateTime; if (typeof params.interval === 'number') { const interval = Luxon.Duration.fromObject({ minutes: params.interval }); to = from.plus(interval); } else if (params.fullDay) { from.set({ hour: 0, minute: 0, second: 0, millisecond: 0 }); to = from.plus(Luxon.Duration.fromObject({ day: 1 })); } else { return null; } if (!to.isValid) return null; return { from: from.toISO(), fromTimestamp: from.toMillis(), to: to.toISO(), toTimestamp: to.toMillis(), fullDay: Boolean(params.fullDay), description: params.alias || cf.name || UNKNOWN_CALENDAR_EVENT, }; } private getCalendarEventsFromSubject( issue: RedmineTypes.ExtendedIssue, ): CalendarEvent[] { const matches = issue.subject.matchAll(/(?<=\()[^()]*(?=\))/g); const items = [...matches].map((i) => i[0]).filter((i) => !!i); return items .map((item) => DatesParser.parseToCalendarEvent(item)) .filter((i) => !!i); } private getCalendarEventsFromDescription( issue: RedmineTypes.ExtendedIssue, ): CalendarEvent[] { const text = issue.description; const lines = text.split('\n').map((line) => line.trim()); const calendarStartIndex = lines.indexOf( this.descriptionCalendarParams.title, ); if (calendarStartIndex < 0) return []; let index = calendarStartIndex + 1; let line = this.extractFromList(lines[index]); if (!line) { index++; line = this.extractFromList(lines[index]); } if (!line) return []; const res: string[] = []; do { res.push(line); index++; line = this.extractFromList(lines[index]); } while (line); return res.map((line) => DatesParser.parseToCalendarEvent(line)); } private extractFromList(line: string): string | null { if (!line || line.length <= 0) return null; const regexp = new RegExp(this.descriptionCalendarParams.lineRegexp); const match = line.match(regexp); return match && match[0] ? match[0] : null; } }