Добавлен парсер персональных сообщений по упоминанию в комментариях
This commit is contained in:
parent
0d7b684244
commit
8c77117703
14 changed files with 172 additions and 65 deletions
|
|
@ -1,8 +1,6 @@
|
|||
import { DynamicModule, Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { EventEmitterService } from './event-emitter.service';
|
||||
import { RedmineEventsGateway } from './events/redmine-events.gateway';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { join } from 'path';
|
||||
import MainConfig from './configs/main-config';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { RedmineDataLoader } from './redmine-data-loader/redmine-data-loader';
|
||||
|
|
@ -41,10 +39,6 @@ export class EventEmitterModule implements OnModuleInit {
|
|||
UsersService,
|
||||
IssuesService,
|
||||
TimestampEnhancer,
|
||||
{
|
||||
provide: 'ENHANCERS',
|
||||
useValue: params?.enhancers || null,
|
||||
},
|
||||
EnhancerService,
|
||||
],
|
||||
exports: [
|
||||
|
|
@ -75,23 +69,25 @@ export class EventEmitterModule implements OnModuleInit {
|
|||
onModuleInit() {
|
||||
const queue = this.redmineEventsGateway.getIssuesChangesQueue();
|
||||
const subj = queue.queue;
|
||||
subj.subscribe(async (issues: any) => {
|
||||
this.logger.debug(`Changed issues = ${JSON.stringify(issues)}`);
|
||||
subj.subscribe(async (issues: RedmineTypes.Issue[]) => {
|
||||
this.logger.debug(
|
||||
`Changed issues - ` +
|
||||
issues.map((i) => `#${i.id} (${i.subject})`).join(', '),
|
||||
);
|
||||
|
||||
for (let i = 0; i < issues.length; i++) {
|
||||
const issue: RedmineTypes.Issue = issues[i];
|
||||
|
||||
try {
|
||||
this.logger.debug(
|
||||
`Save issue #${issue.id} - ${JSON.stringify(issue)}`,
|
||||
);
|
||||
this.logger.debug(`Save issue #${issue.id} (${issue.subject})`);
|
||||
|
||||
const response = await this.redmineIssuesCacheWriterService.saveIssue(
|
||||
issue,
|
||||
);
|
||||
|
||||
this.logger.debug(
|
||||
`Save issue #${issue.id} response = ${JSON.stringify(response)}`,
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
`Save ${(response.prev) ? 'exists' : 'new'} issue #${issue.id} - count of changes = ${response.journalsDiff.length}`,
|
||||
);
|
||||
} catch (ex) {
|
||||
this.logger.error(`Saving issue error - ${ex}`, null, {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { WebhookConfigItemModel } from '../models/webhook-config-item-model';
|
|||
import { RssListener } from '../rsslistener/rsslistener';
|
||||
import { EventsListener } from './events-listener';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { RedmineTypes } from '../models/redmine-types';
|
||||
|
||||
type IssuesChangesQueueParams = {
|
||||
updateInterval: number;
|
||||
|
|
@ -48,14 +49,14 @@ export class RedmineEventsGateway {
|
|||
issuesChanges(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@MessageBody() data: any,
|
||||
): Observable<WsResponse<RedmineIssueData[]>> {
|
||||
): Observable<WsResponse<RedmineTypes.Issue[]>> {
|
||||
return this.issuesChangesObservable;
|
||||
}
|
||||
|
||||
private issuesChangesQueue: Queue<number, RedmineIssueData>;
|
||||
getIssuesChangesQueue(): Queue<number, RedmineIssueData> {
|
||||
private issuesChangesQueue: Queue<number, RedmineTypes.Issue>;
|
||||
getIssuesChangesQueue(): Queue<number, RedmineTypes.Issue> {
|
||||
if (!this.issuesChangesQueue) {
|
||||
this.issuesChangesQueue = new Queue<number, RedmineIssueData>(
|
||||
this.issuesChangesQueue = new Queue<number, RedmineTypes.Issue>(
|
||||
this.issuesChangesQueueParams.updateInterval,
|
||||
this.issuesChangesQueueParams.itemsLimit,
|
||||
async (issueNumbers) => {
|
||||
|
|
|
|||
|
|
@ -17,13 +17,11 @@ export class RedmineIssuesCacheWriterService {
|
|||
|
||||
async saveIssue(issue: RedmineTypes.Issue): Promise<SaveResponse> {
|
||||
this.logger.debug(
|
||||
`Saving issue ${issue?.id || '-'} - ${
|
||||
issue?.subject || '-'
|
||||
}, issue data = ${JSON.stringify(issue)}`,
|
||||
`Saving issue ${issue?.id || '-'} (${issue?.subject || '-'})`,
|
||||
);
|
||||
const id = Number(issue['id']);
|
||||
let prevIssue: (nano.DocumentGetResponse & RedmineTypes.Issue) | null;
|
||||
const issueDb = await Issues.getDatasource();
|
||||
const issueDb = await this.issues.getDatasource();
|
||||
if (!issueDb) {
|
||||
throw `CouchDb datasource must defined`;
|
||||
}
|
||||
|
|
@ -46,7 +44,7 @@ export class RedmineIssuesCacheWriterService {
|
|||
journalsDiff: this.getJournalsDiff(prevIssue, newIssue),
|
||||
};
|
||||
this.logger.debug(
|
||||
`Saving issue success ${issue?.id || '-'} - ${issue?.subject || '-'}`,
|
||||
`Saving issue success #${issue?.id || '-'} - ${issue?.subject || '-'}`,
|
||||
);
|
||||
this.subject.next(res);
|
||||
return res;
|
||||
|
|
|
|||
|
|
@ -1,31 +1,19 @@
|
|||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { RedmineTypes } from '../models/redmine-types';
|
||||
import { IssueEnhancerInterface } from './issue-enhancer-interface';
|
||||
|
||||
@Injectable()
|
||||
export class EnhancerService {
|
||||
private logger = new Logger(EnhancerService.name);
|
||||
private initilialized = false;
|
||||
private enhancers: IssueEnhancerInterface[] = [];
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
@Inject('ENHANCERS')
|
||||
private enhancers: IssueEnhancerInterface[],
|
||||
) {}
|
||||
|
||||
private init(): void {
|
||||
if (!this.initilialized) {
|
||||
this.logger.log(`Initialize EnhancerService start`);
|
||||
this.enhancers.forEach((e) => e.init(this.moduleRef));
|
||||
this.logger.log(`Initialize EnhancerService finished`);
|
||||
}
|
||||
addEnhancer(enh: IssueEnhancerInterface[]): void {
|
||||
this.enhancers.push(...enh);
|
||||
}
|
||||
|
||||
async enhanceIssue(
|
||||
issue: RedmineTypes.Issue & Record<string, any>,
|
||||
): Promise<RedmineTypes.Issue & Record<string, any>> {
|
||||
this.init();
|
||||
for (let i = 0; i < this.enhancers.length; i++) {
|
||||
const enhancer = this.enhancers[i];
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { ModuleRef } from '@nestjs/core';
|
||||
import { RedmineTypes } from '../models/redmine-types';
|
||||
|
||||
export interface IssueEnhancerInterface {
|
||||
name: string;
|
||||
init(moduleRef: ModuleRef);
|
||||
enhance(
|
||||
issue: RedmineTypes.Issue,
|
||||
): Promise<RedmineTypes.Issue & Record<string, any>>;
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@ import { IssueEnhancerInterface } from './issue-enhancer-interface';
|
|||
export class TimestampEnhancer implements IssueEnhancerInterface {
|
||||
name = 'timestamp';
|
||||
|
||||
init() {
|
||||
return;
|
||||
}
|
||||
|
||||
async enhance(
|
||||
issue: RedmineTypes.Issue,
|
||||
): Promise<RedmineTypes.Issue & Record<string, any>> {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { IssueEnhancerInterface } from '../issue-enhancers/issue-enhancer-interface';
|
||||
import { MainConfigModel } from './main-config-model';
|
||||
|
||||
export type ModuleParams = {
|
||||
config?: MainConfigModel;
|
||||
enhancers?: IssueEnhancerInterface[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -46,6 +46,32 @@ export class UsersService {
|
|||
);
|
||||
}
|
||||
|
||||
async findUserByName(
|
||||
firstname: string,
|
||||
lastname: string,
|
||||
): Promise<RedmineTypes.User | null> {
|
||||
const userFromMemoryCache = this.memoryCache.find((item) => {
|
||||
return item.firstname === firstname && item.lastname === lastname;
|
||||
});
|
||||
if (userFromMemoryCache) {
|
||||
return RedmineTypes.CreateUser(userFromMemoryCache);
|
||||
}
|
||||
const usersDb = await this.users.getDatasource();
|
||||
const res = await usersDb.find({
|
||||
selector: {
|
||||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
},
|
||||
limit: 1,
|
||||
});
|
||||
if (!res && !res.docs && !res.docs[0]) {
|
||||
return null;
|
||||
}
|
||||
const userFromDb = res.docs[0];
|
||||
this.memoryCache.set(userFromDb.id, userFromDb);
|
||||
return RedmineTypes.CreateUser(userFromDb);
|
||||
}
|
||||
|
||||
private async getUserFromRedmine(
|
||||
userId: number,
|
||||
): Promise<RedmineTypes.User | null> {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,22 @@ export class MemoryCache<K, T> {
|
|||
}
|
||||
}
|
||||
|
||||
find(fn: (item: T, key: K) => boolean): (T & Timestamped) | null {
|
||||
for (const key in this.memoryCache) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.memoryCache, key)) {
|
||||
const item = this.memoryCache[key];
|
||||
if (TimestampIsTimeouted(item, this.timeout)) {
|
||||
delete this.memoryCache[key];
|
||||
continue;
|
||||
}
|
||||
if (fn(item, key as any)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private startAutoclean() {
|
||||
setTimeout(() => {
|
||||
this.cleanTimeouted();
|
||||
|
|
|
|||
|
|
@ -1,38 +1,58 @@
|
|||
import { EventEmitterModule } from '@app/event-emitter';
|
||||
import { RedmineIssuesCacheWriterService } from '@app/event-emitter/issue-cache-writer/redmine-issues-cache-writer.service';
|
||||
import { EnhancerService } from '@app/event-emitter/issue-enhancers/enhancer.service';
|
||||
import { TimestampEnhancer } from '@app/event-emitter/issue-enhancers/timestamps-enhancer';
|
||||
import { MainController } from '@app/event-emitter/main/main.controller';
|
||||
import { UsersService } from '@app/event-emitter/users/users.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import configuration from './configs/app';
|
||||
import { CurrentUserEnhancer } from './issue-enhancers/current-user-enhancer';
|
||||
import { CustomFieldsEnhancer } from './issue-enhancers/custom-fields-enhancer';
|
||||
import { PersonalNotificationsService } from './notifications/personal-notifications.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
EventEmitterModule.register({
|
||||
config: configuration().redmineIssueEventEmitterConfig,
|
||||
enhancers: [
|
||||
new TimestampEnhancer(),
|
||||
new CustomFieldsEnhancer(),
|
||||
new CurrentUserEnhancer(),
|
||||
],
|
||||
}),
|
||||
ConfigModule.forRoot({ load: [configuration] }),
|
||||
],
|
||||
controllers: [AppController, MainController],
|
||||
providers: [
|
||||
AppService,
|
||||
UsersService,
|
||||
CustomFieldsEnhancer,
|
||||
CurrentUserEnhancer,
|
||||
PersonalNotificationsService,
|
||||
],
|
||||
})
|
||||
export class AppModule {
|
||||
export class AppModule implements OnModuleInit {
|
||||
private logger = new Logger(AppModule.name);
|
||||
|
||||
constructor(
|
||||
private personalNotificationsService: PersonalNotificationsService,
|
||||
private redmineIssuesCacheWriterService: RedmineIssuesCacheWriterService,
|
||||
private enhancerService: EnhancerService,
|
||||
private timestampEnhancer: TimestampEnhancer,
|
||||
private customFieldsEnhancer: CustomFieldsEnhancer,
|
||||
private currentUserEnhancer: CurrentUserEnhancer,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.enhancerService.addEnhancer([
|
||||
this.timestampEnhancer,
|
||||
this.customFieldsEnhancer,
|
||||
this.currentUserEnhancer,
|
||||
]);
|
||||
this.personalNotificationsService.$messages.subscribe((message) => {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
this.logger.log(`Get personal message ${JSON.stringify(message.message)} for recipients ${JSON.stringify(message.recipients)}`);
|
||||
});
|
||||
this.redmineIssuesCacheWriterService.subject.subscribe(
|
||||
async (saveResult) => {
|
||||
await this.personalNotificationsService.analize(saveResult);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,6 @@ export class CurrentUserEnhancer implements IssueEnhancerInterface {
|
|||
Rejected: 'dev',
|
||||
};
|
||||
|
||||
init() {
|
||||
return;
|
||||
}
|
||||
|
||||
async enhance(
|
||||
issue: RedmineTypes.Issue,
|
||||
): Promise<RedmineTypes.Issue & Record<string, any>> {
|
||||
|
|
|
|||
|
|
@ -2,17 +2,11 @@ import { IssueEnhancerInterface } from '@app/event-emitter/issue-enhancers/issue
|
|||
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
|
||||
import { UsersService } from '@app/event-emitter/users/users.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
|
||||
@Injectable()
|
||||
export class CustomFieldsEnhancer implements IssueEnhancerInterface {
|
||||
name = 'custom-fields';
|
||||
|
||||
private usersService: UsersService;
|
||||
|
||||
init(moduleRef: ModuleRef): void {
|
||||
this.usersService = moduleRef.get(UsersService);
|
||||
}
|
||||
constructor(private usersService: UsersService) {}
|
||||
|
||||
async enhance(
|
||||
issue: RedmineTypes.Issue,
|
||||
|
|
|
|||
5
src/models/personal-parsed-message.model.ts
Normal file
5
src/models/personal-parsed-message.model.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export class PersonalParsedMessage {
|
||||
sender: number;
|
||||
message: string;
|
||||
recipients: number[];
|
||||
}
|
||||
75
src/notifications/personal-notifications.service.ts
Normal file
75
src/notifications/personal-notifications.service.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
|
||||
import { SaveResponse } from '@app/event-emitter/models/save-response';
|
||||
import { UsersService } from '@app/event-emitter/users/users.service';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Subject } from 'rxjs';
|
||||
import { PersonalParsedMessage } from 'src/models/personal-parsed-message.model';
|
||||
|
||||
@Injectable()
|
||||
export class PersonalNotificationsService {
|
||||
private userNameRe = /@([\wА-Яа-яЁё]+) ([\wА-Яа-яЁё]+)@/g;
|
||||
private logger = new Logger(PersonalNotificationsService.name);
|
||||
|
||||
$messages = new Subject<PersonalParsedMessage>();
|
||||
|
||||
constructor(private usersService: UsersService) {}
|
||||
|
||||
async analize(data: SaveResponse): Promise<PersonalParsedMessage[]> {
|
||||
this.logger.debug(
|
||||
`Analize personal messages for issue ` +
|
||||
`#${data.current.id} (${data.current.subject}) start`,
|
||||
);
|
||||
const pMessages = data.journalsDiff.map(async (journal) => {
|
||||
const message = await this.getMessage(journal);
|
||||
if (message) {
|
||||
this.logger.log(
|
||||
`Found personal message in issue #${data.current.id} ` +
|
||||
`(${data.current.subject}) - ` +
|
||||
`message = ${JSON.stringify(message.message)} ` +
|
||||
`from sender ${message.sender} ` +
|
||||
`for recipients ${JSON.stringify(message.recipients)}`,
|
||||
);
|
||||
this.$messages.next(message);
|
||||
}
|
||||
return message;
|
||||
});
|
||||
const res = (await Promise.all(pMessages)).filter((m) => Boolean(m));
|
||||
this.logger.debug(
|
||||
`Analize personal messages for issue ` +
|
||||
`#${data.current.id} (${data.current.subject}) finished`,
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
private async getMessage(
|
||||
journal: RedmineTypes.Journal,
|
||||
): Promise<PersonalParsedMessage | null> {
|
||||
if (!journal.notes) {
|
||||
return null;
|
||||
}
|
||||
const notes = journal.notes;
|
||||
const results = notes.matchAll(this.userNameRe);
|
||||
const recipients: number[] = [];
|
||||
let result = results.next();
|
||||
while (!result.done) {
|
||||
if (result.value && result.value[1] && result.value[2]) {
|
||||
const firstname = result.value[1];
|
||||
const lastname = result.value[2];
|
||||
const user = await this.usersService.findUserByName(
|
||||
firstname,
|
||||
lastname,
|
||||
);
|
||||
if (user) recipients.push(user.id);
|
||||
}
|
||||
result = results.next();
|
||||
}
|
||||
if (recipients.length > 0) {
|
||||
return {
|
||||
message: notes,
|
||||
sender: journal.user.id,
|
||||
recipients: recipients,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue