Добавлен парсер персональных сообщений по упоминанию в комментариях

This commit is contained in:
Pavel Gnedov 2022-07-23 20:41:00 +07:00
parent 0d7b684244
commit 8c77117703
14 changed files with 172 additions and 65 deletions

View file

@ -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, {

View file

@ -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) => {

View file

@ -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;

View file

@ -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

View file

@ -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>>;

View file

@ -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>> {

View file

@ -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[];
};

View file

@ -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> {

View file

@ -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();

View file

@ -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);
},
);
}
}

View file

@ -20,10 +20,6 @@ export class CurrentUserEnhancer implements IssueEnhancerInterface {
Rejected: 'dev',
};
init() {
return;
}
async enhance(
issue: RedmineTypes.Issue,
): Promise<RedmineTypes.Issue & Record<string, any>> {

View file

@ -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,

View file

@ -0,0 +1,5 @@
export class PersonalParsedMessage {
sender: number;
message: string;
recipients: number[];
}

View 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;
}
}