195 lines
5.4 KiB
TypeScript
195 lines
5.4 KiB
TypeScript
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<RedmineTypes.ExtendedIssue> {
|
|
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;
|
|
}
|
|
}
|