Merge branch 'dev'
This commit is contained in:
commit
93e4ea0778
15 changed files with 7765 additions and 2642 deletions
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"editor.tabSize": 2
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
{
|
{
|
||||||
"couchDb": {
|
"couchDb": {
|
||||||
"dbs": {
|
"dbs": {
|
||||||
"changes": ""
|
"changes": "",
|
||||||
}
|
"userMetaInfo": "",
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"telegramBotToken": "",
|
||||||
|
"personalMessageTemplate": ""
|
||||||
}
|
}
|
||||||
9941
package-lock.json
generated
9941
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -29,9 +29,11 @@
|
||||||
"@nestjs/serve-static": "^2.2.2",
|
"@nestjs/serve-static": "^2.2.2",
|
||||||
"@nestjs/websockets": "^8.4.4",
|
"@nestjs/websockets": "^8.4.4",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
"cache-manager": "^4.1.0",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"imap-simple": "^5.1.0",
|
"imap-simple": "^5.1.0",
|
||||||
"nano": "^10.0.0",
|
"nano": "^10.0.0",
|
||||||
|
"node-telegram-bot-api": "^0.59.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rss-parser": "^3.12.0",
|
"rss-parser": "^3.12.0",
|
||||||
|
|
@ -42,9 +44,11 @@
|
||||||
"@nestjs/cli": "^8.0.0",
|
"@nestjs/cli": "^8.0.0",
|
||||||
"@nestjs/schematics": "^8.0.0",
|
"@nestjs/schematics": "^8.0.0",
|
||||||
"@nestjs/testing": "^8.0.0",
|
"@nestjs/testing": "^8.0.0",
|
||||||
|
"@types/cache-manager": "^4.0.1",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/node": "^16.0.0",
|
"@types/node": "^16.0.0",
|
||||||
|
"@types/node-telegram-bot-api": "^0.57.1",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ import { RedmineIssuesCacheWriterService } from '@app/event-emitter/issue-cache-
|
||||||
import { EnhancerService } from '@app/event-emitter/issue-enhancers/enhancer.service';
|
import { EnhancerService } from '@app/event-emitter/issue-enhancers/enhancer.service';
|
||||||
import { TimestampEnhancer } from '@app/event-emitter/issue-enhancers/timestamps-enhancer';
|
import { TimestampEnhancer } from '@app/event-emitter/issue-enhancers/timestamps-enhancer';
|
||||||
import { MainController } from '@app/event-emitter/main/main.controller';
|
import { MainController } from '@app/event-emitter/main/main.controller';
|
||||||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
import { CacheModule, Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { switchMap } from 'rxjs';
|
import { switchMap, tap } from 'rxjs';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import configuration from './configs/app';
|
import configuration from './configs/app';
|
||||||
|
|
@ -18,6 +18,12 @@ import { StatusChangeNotificationsService } from './notifications/status-change-
|
||||||
import { ChangesCacheWriterService } from './changes-cache-writer/changes-cache-writer.service';
|
import { ChangesCacheWriterService } from './changes-cache-writer/changes-cache-writer.service';
|
||||||
import { Issues } from '@app/event-emitter/couchdb-datasources/issues';
|
import { Issues } from '@app/event-emitter/couchdb-datasources/issues';
|
||||||
import { Users } from '@app/event-emitter/couchdb-datasources/users';
|
import { Users } from '@app/event-emitter/couchdb-datasources/users';
|
||||||
|
import { TelegramBotService } from './telegram-bot/telegram-bot.service';
|
||||||
|
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 { PublicUrlAdapterService } from './notifications/adapters/public-url.adapter.service';
|
||||||
|
import { StatusChangeAdapterService } from './notifications/adapters/status-change.adapter.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -25,6 +31,9 @@ import { Users } from '@app/event-emitter/couchdb-datasources/users';
|
||||||
config: configuration().redmineIssueEventEmitterConfig,
|
config: configuration().redmineIssueEventEmitterConfig,
|
||||||
}),
|
}),
|
||||||
ConfigModule.forRoot({ load: [configuration] }),
|
ConfigModule.forRoot({ load: [configuration] }),
|
||||||
|
CacheModule.register({
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
controllers: [AppController, MainController],
|
controllers: [AppController, MainController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|
@ -36,6 +45,12 @@ import { Users } from '@app/event-emitter/couchdb-datasources/users';
|
||||||
Changes,
|
Changes,
|
||||||
RedminePublicUrlConverter,
|
RedminePublicUrlConverter,
|
||||||
ChangesCacheWriterService,
|
ChangesCacheWriterService,
|
||||||
|
TelegramBotService,
|
||||||
|
UserMetaInfoService,
|
||||||
|
UserMetaInfo,
|
||||||
|
PersonalNotificationAdapterService,
|
||||||
|
PublicUrlAdapterService,
|
||||||
|
StatusChangeAdapterService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule implements OnModuleInit {
|
export class AppModule implements OnModuleInit {
|
||||||
|
|
@ -50,12 +65,16 @@ export class AppModule implements OnModuleInit {
|
||||||
private currentUserEnhancer: CurrentUserEnhancer,
|
private currentUserEnhancer: CurrentUserEnhancer,
|
||||||
private statusChangeNotificationsService: StatusChangeNotificationsService,
|
private statusChangeNotificationsService: StatusChangeNotificationsService,
|
||||||
private changesCacheWriterService: ChangesCacheWriterService,
|
private changesCacheWriterService: ChangesCacheWriterService,
|
||||||
|
private telegramBotService: TelegramBotService,
|
||||||
|
private personalNotificationAdapterService: PersonalNotificationAdapterService,
|
||||||
|
private statusChangeAdapterService: StatusChangeAdapterService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
Issues.getDatasource();
|
Issues.getDatasource();
|
||||||
Users.getDatasource();
|
Users.getDatasource();
|
||||||
Changes.getDatasource();
|
Changes.getDatasource();
|
||||||
|
UserMetaInfo.getDatasource();
|
||||||
|
|
||||||
this.enhancerService.addEnhancer([
|
this.enhancerService.addEnhancer([
|
||||||
this.timestampEnhancer,
|
this.timestampEnhancer,
|
||||||
|
|
@ -63,33 +82,37 @@ export class AppModule implements OnModuleInit {
|
||||||
this.currentUserEnhancer,
|
this.currentUserEnhancer,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.personalNotificationsService.$messages.subscribe((message) => {
|
this.personalNotificationsService.$messages.subscribe((resp) => {
|
||||||
// eslint-disable-next-line prettier/prettier
|
this.logger.log(
|
||||||
this.logger.log(`Get personal message ${JSON.stringify(message.message)} for recipients ${JSON.stringify(message.recipients)}`);
|
`Get personal message ` +
|
||||||
|
JSON.stringify(resp.personalParsedMessage.message) +
|
||||||
|
` for recipients ` +
|
||||||
|
JSON.stringify(resp.personalParsedMessage.recipients),
|
||||||
|
);
|
||||||
|
this.personalNotificationAdapterService.send(resp);
|
||||||
});
|
});
|
||||||
this.statusChangeNotificationsService.$changes.subscribe((change) => {
|
this.statusChangeNotificationsService.$changes.subscribe((change) => {
|
||||||
|
const messages = change.messages
|
||||||
|
.map((m) => m.change_message)
|
||||||
|
.filter((m) => !!m);
|
||||||
|
const notifications = change.messages
|
||||||
|
.map((m) => m.notification_message)
|
||||||
|
.filter((m) => !!m);
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Get status changes messages for ` +
|
`Get status changes messages for ` +
|
||||||
`issue_id = ${change.issue_id}, ` +
|
`issue_id = ${change.issue_id}, ` +
|
||||||
`messages = ${JSON.stringify(
|
`messages = ${JSON.stringify(messages)}, ` +
|
||||||
change.messages.map((m) => m.change_message).filter((m) => !!m),
|
`notifications = ${JSON.stringify(notifications)}`,
|
||||||
)}`,
|
|
||||||
);
|
);
|
||||||
|
this.statusChangeAdapterService.send(change);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.redmineIssuesCacheWriterService.subject.subscribe(
|
|
||||||
async (saveResult) => {
|
|
||||||
await this.personalNotificationsService.analize(saveResult);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
this.redmineIssuesCacheWriterService.subject
|
this.redmineIssuesCacheWriterService.subject
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(async (saveResult) => {
|
tap((saveResult) => {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Save result process started, issue_id = ${saveResult.current.id}`,
|
`Save result process started, issue_id = ${saveResult.current.id}`,
|
||||||
);
|
);
|
||||||
return saveResult;
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
@ -117,6 +140,10 @@ export class AppModule implements OnModuleInit {
|
||||||
this.logger.debug('Save changes in couchdb successed');
|
this.logger.debug('Save changes in couchdb successed');
|
||||||
return args;
|
return args;
|
||||||
}),
|
}),
|
||||||
|
switchMap(async (args) => {
|
||||||
|
this.logger.debug(``);
|
||||||
|
return args;
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.subscribe(async (args) => {
|
.subscribe(async (args) => {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
|
|
|
||||||
35
src/couchdb-datasources/user-meta-info.ts
Normal file
35
src/couchdb-datasources/user-meta-info.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { CouchDb } from '@app/event-emitter/couchdb-datasources/couchdb';
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import nano from 'nano';
|
||||||
|
import { UserMetaInfoModel } from 'src/models/user-meta-info.model';
|
||||||
|
import configuration from '../configs/app';
|
||||||
|
|
||||||
|
const config = configuration();
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserMetaInfo {
|
||||||
|
private static logger = new Logger(UserMetaInfo.name);
|
||||||
|
private static db = null;
|
||||||
|
|
||||||
|
static async getDatasource(): Promise<nano.DocumentScope<UserMetaInfoModel>> {
|
||||||
|
if (UserMetaInfo.db) {
|
||||||
|
return UserMetaInfo.db;
|
||||||
|
}
|
||||||
|
const n = CouchDb.getCouchDb();
|
||||||
|
const userMetaInfoDbName =
|
||||||
|
config.couchDb.dbs.userMetaInfo || 'user_meta_info';
|
||||||
|
const dbs = await n.db.list();
|
||||||
|
if (!dbs.includes(userMetaInfoDbName)) {
|
||||||
|
await n.db.create(userMetaInfoDbName);
|
||||||
|
}
|
||||||
|
UserMetaInfo.db = await n.db.use(userMetaInfoDbName);
|
||||||
|
UserMetaInfo.logger.log(
|
||||||
|
`Connected to user_meta_info db - ${userMetaInfoDbName}`,
|
||||||
|
);
|
||||||
|
return UserMetaInfo.db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDatasource(): Promise<nano.DocumentScope<UserMetaInfoModel>> {
|
||||||
|
return await UserMetaInfo.getDatasource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,8 @@ export type AppConfig = {
|
||||||
couchDb: {
|
couchDb: {
|
||||||
dbs: {
|
dbs: {
|
||||||
changes: string;
|
changes: string;
|
||||||
|
userMetaInfo: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
telegramBotToken: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
7
src/models/issue-and-personal-parsed-message.model.ts
Normal file
7
src/models/issue-and-personal-parsed-message.model.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
|
||||||
|
import { PersonalParsedMessage } from './personal-parsed-message.model';
|
||||||
|
|
||||||
|
export type IssueAndPersonalParsedMessageModel = {
|
||||||
|
issue: RedmineTypes.Issue;
|
||||||
|
personalParsedMessage: PersonalParsedMessage;
|
||||||
|
};
|
||||||
4
src/models/user-meta-info.model.ts
Normal file
4
src/models/user-meta-info.model.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export type UserMetaInfoModel = {
|
||||||
|
user_id: number;
|
||||||
|
telegram_chat_id: number;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { UsersService } from '@app/event-emitter/users/users.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { IssueAndPersonalParsedMessageModel } from 'src/models/issue-and-personal-parsed-message.model';
|
||||||
|
import { TelegramBotService } from 'src/telegram-bot/telegram-bot.service';
|
||||||
|
import Handlebars from 'handlebars';
|
||||||
|
import { PublicUrlAdapterService } from '../public-url.adapter.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PersonalNotificationAdapterService {
|
||||||
|
private personalMessageTemplate: HandlebarsTemplateDelegate;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private telegramBotService: TelegramBotService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
private usersService: UsersService,
|
||||||
|
private publicUrlAdapterService: PublicUrlAdapterService,
|
||||||
|
) {
|
||||||
|
const template = this.configService.get<string>('personalMessageTemplate');
|
||||||
|
this.personalMessageTemplate = Handlebars.compile(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(
|
||||||
|
issueAndMessages: IssueAndPersonalParsedMessageModel,
|
||||||
|
): Promise<void> {
|
||||||
|
const promises = issueAndMessages.personalParsedMessage.recipients.map(
|
||||||
|
async (recipient) => {
|
||||||
|
const redmineId = recipient;
|
||||||
|
const issueUrlHtml = this.publicUrlAdapterService.getHtmlHref(
|
||||||
|
issueAndMessages.issue,
|
||||||
|
);
|
||||||
|
const sender = await this.usersService.getUser(
|
||||||
|
issueAndMessages.personalParsedMessage.sender,
|
||||||
|
);
|
||||||
|
const data = {
|
||||||
|
data: {
|
||||||
|
issue: issueAndMessages.issue,
|
||||||
|
messages: issueAndMessages.personalParsedMessage,
|
||||||
|
},
|
||||||
|
issue_url: issueUrlHtml,
|
||||||
|
sender_name: sender.name,
|
||||||
|
message: issueAndMessages.personalParsedMessage.message,
|
||||||
|
};
|
||||||
|
const message = this.personalMessageTemplate(data);
|
||||||
|
return await this.telegramBotService.sendMessageByRedmineId(
|
||||||
|
redmineId,
|
||||||
|
message,
|
||||||
|
{ parse_mode: 'HTML' },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/notifications/adapters/public-url.adapter.service.ts
Normal file
21
src/notifications/adapters/public-url.adapter.service.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PublicUrlAdapterService {
|
||||||
|
private publicUrl: string;
|
||||||
|
|
||||||
|
constructor(private configService: ConfigService) {
|
||||||
|
this.publicUrl = this.configService.get<string>('redmineUrlPublic');
|
||||||
|
}
|
||||||
|
|
||||||
|
getUrl(issueId: number): string {
|
||||||
|
return `${this.publicUrl}/issues/${issueId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHtmlHref(issue: RedmineTypes.Issue): string {
|
||||||
|
const url = this.getUrl(issue.id);
|
||||||
|
return `<a href="${url}">${issue.tracker.name} #${issue.id}</a>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/notifications/adapters/status-change.adapter.service.ts
Normal file
22
src/notifications/adapters/status-change.adapter.service.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Change } from 'src/models/change.model';
|
||||||
|
import { TelegramBotService } from 'src/telegram-bot/telegram-bot.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class StatusChangeAdapterService {
|
||||||
|
constructor(private telegramBotService: TelegramBotService) {}
|
||||||
|
|
||||||
|
async send(change: Change): Promise<boolean[]> {
|
||||||
|
const promises = change.messages.map((m) => {
|
||||||
|
if (!m || !m.recipient || !m.recipient.id || !m.notification_message) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.telegramBotService.sendMessageByRedmineId(
|
||||||
|
m.recipient.id,
|
||||||
|
m.notification_message,
|
||||||
|
{ parse_mode: 'HTML' },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return await Promise.all(promises);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import { SaveResponse } from '@app/event-emitter/models/save-response';
|
||||||
import { UsersService } from '@app/event-emitter/users/users.service';
|
import { UsersService } from '@app/event-emitter/users/users.service';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
import { IssueAndPersonalParsedMessageModel } from 'src/models/issue-and-personal-parsed-message.model';
|
||||||
import { PersonalParsedMessage } from 'src/models/personal-parsed-message.model';
|
import { PersonalParsedMessage } from 'src/models/personal-parsed-message.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -10,11 +11,13 @@ export class PersonalNotificationsService {
|
||||||
private userNameRe = /@([\wА-Яа-яЁё]+) ([\wА-Яа-яЁё]+)@/g;
|
private userNameRe = /@([\wА-Яа-яЁё]+) ([\wА-Яа-яЁё]+)@/g;
|
||||||
private logger = new Logger(PersonalNotificationsService.name);
|
private logger = new Logger(PersonalNotificationsService.name);
|
||||||
|
|
||||||
$messages = new Subject<PersonalParsedMessage>();
|
$messages = new Subject<IssueAndPersonalParsedMessageModel>();
|
||||||
|
|
||||||
constructor(private usersService: UsersService) {}
|
constructor(private usersService: UsersService) {}
|
||||||
|
|
||||||
async analize(data: SaveResponse): Promise<PersonalParsedMessage[]> {
|
async analize(
|
||||||
|
data: SaveResponse,
|
||||||
|
): Promise<IssueAndPersonalParsedMessageModel[]> {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Analize personal messages for issue ` +
|
`Analize personal messages for issue ` +
|
||||||
`#${data.current.id} (${data.current.subject}) start`,
|
`#${data.current.id} (${data.current.subject}) start`,
|
||||||
|
|
@ -29,11 +32,16 @@ export class PersonalNotificationsService {
|
||||||
`from sender ${message.sender} ` +
|
`from sender ${message.sender} ` +
|
||||||
`for recipients ${JSON.stringify(message.recipients)}`,
|
`for recipients ${JSON.stringify(message.recipients)}`,
|
||||||
);
|
);
|
||||||
this.$messages.next(message);
|
this.$messages.next({
|
||||||
|
issue: data.current,
|
||||||
|
personalParsedMessage: message,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
});
|
});
|
||||||
const res = (await Promise.all(pMessages)).filter((m) => Boolean(m));
|
const res = (await Promise.all(pMessages))
|
||||||
|
.filter((m) => Boolean(m))
|
||||||
|
.map((m) => ({ issue: data.current, personalParsedMessage: m }));
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Analize personal messages for issue ` +
|
`Analize personal messages for issue ` +
|
||||||
`#${data.current.id} (${data.current.subject}) finished`,
|
`#${data.current.id} (${data.current.subject}) finished`,
|
||||||
|
|
|
||||||
165
src/telegram-bot/telegram-bot.service.ts
Normal file
165
src/telegram-bot/telegram-bot.service.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import { UsersService } from '@app/event-emitter/users/users.service';
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
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';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TelegramBotService {
|
||||||
|
private logger = new Logger(TelegramBotService.name);
|
||||||
|
private bot: TelegramBot;
|
||||||
|
private telegramBotToken: string;
|
||||||
|
private redmineApiUrlPrefix: string;
|
||||||
|
private redminePublicUrlPrefix: string;
|
||||||
|
|
||||||
|
private registerRe = /\/register (\d+) (.+)/;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private userMetaInfoService: UserMetaInfoService,
|
||||||
|
private usersService: UsersService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {
|
||||||
|
this.telegramBotToken = this.configService.get<string>('telegramBotToken');
|
||||||
|
this.redmineApiUrlPrefix =
|
||||||
|
this.configService.get<string>('redmineUrlPrefix');
|
||||||
|
this.redminePublicUrlPrefix =
|
||||||
|
this.configService.get<string>('redmineUrlPublic');
|
||||||
|
this.initTelegramBot();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initTelegramBot(): Promise<void> {
|
||||||
|
const Telegram = await require('node-telegram-bot-api');
|
||||||
|
this.bot = new Telegram(this.telegramBotToken, { polling: true });
|
||||||
|
this.bot.onText(/\/start/, async (msg) => {
|
||||||
|
await this.showHelpMessage(msg);
|
||||||
|
});
|
||||||
|
this.bot.onText(/\/help/, async (msg) => {
|
||||||
|
await this.showHelpMessage(msg);
|
||||||
|
});
|
||||||
|
this.bot.onText(this.registerRe, async (msg) => {
|
||||||
|
await this.register(msg);
|
||||||
|
});
|
||||||
|
this.bot.onText(/\/leave/, async (msg) => {
|
||||||
|
await this.leave(msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showHelpMessage(msg: TelegramBot.Message): Promise<void> {
|
||||||
|
const userMetaInfo = await this.userMetaInfoService.findByTelegramId(
|
||||||
|
msg.chat.id,
|
||||||
|
);
|
||||||
|
let helpMessage: string;
|
||||||
|
if (userMetaInfo) {
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
helpMessage = [
|
||||||
|
`/current_issues - мои текущие задачи`,
|
||||||
|
`/help`
|
||||||
|
].join('\n');
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
helpMessage = [
|
||||||
|
`/register <redmine_id> <redmine_token>`,
|
||||||
|
`/help`
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
this.logger.debug(
|
||||||
|
`Sent help message for telegramChatId = ${msg.chat.id}, ` +
|
||||||
|
`message = ${helpMessage}`,
|
||||||
|
);
|
||||||
|
this.bot.sendMessage(msg.chat.id, helpMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessageByRedmineId(
|
||||||
|
redmineId: number,
|
||||||
|
msg: string,
|
||||||
|
options?: TelegramBot.SendMessageOptions,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const userMetaInfo = await this.userMetaInfoService.findByRedmineId(
|
||||||
|
redmineId,
|
||||||
|
);
|
||||||
|
if (!userMetaInfo) return false;
|
||||||
|
const chatId = userMetaInfo.telegram_chat_id;
|
||||||
|
await this.bot.sendMessage(chatId, msg, options);
|
||||||
|
this.logger.debug(
|
||||||
|
`Sent message for redmineUserId = ${redmineId}, ` +
|
||||||
|
`telegramChatId = ${chatId}, ` +
|
||||||
|
`message = ${msg}, ` +
|
||||||
|
`options = ${JSON.stringify(options)}`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessageByName(
|
||||||
|
firstname: string,
|
||||||
|
lastname: string,
|
||||||
|
msg: string,
|
||||||
|
options?: TelegramBot.SendMessageOptions,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const user = await this.usersService.findUserByName(firstname, lastname);
|
||||||
|
if (!user) return false;
|
||||||
|
return await this.sendMessageByRedmineId(user.id, msg, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async register(
|
||||||
|
msg: TelegramBot.Message,
|
||||||
|
): Promise<{ result: boolean; message: string }> {
|
||||||
|
const items = (msg.text || '').match(this.registerRe);
|
||||||
|
|
||||||
|
const result = (result: boolean, message: string, logData?: any) => {
|
||||||
|
const logMsg =
|
||||||
|
`Telegram registration ${result ? 'successed' : 'failed'} ` +
|
||||||
|
`with message ${message}, ` +
|
||||||
|
`log data = ${JSON.stringify(logData || null)}`;
|
||||||
|
this.logger.log(logMsg);
|
||||||
|
this.bot.sendMessage(msg.chat.id, message);
|
||||||
|
return { result: result, message: message };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!items || !items[1] || !items[2]) {
|
||||||
|
return result(false, 'Не указаны необходимые данные');
|
||||||
|
}
|
||||||
|
|
||||||
|
let redmineId: number, redmineToken: string;
|
||||||
|
try {
|
||||||
|
redmineId = Number(items[1]);
|
||||||
|
redmineToken = items[2];
|
||||||
|
} catch (ex) {
|
||||||
|
return result(false, 'Введены неверные значения', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${this.redminePublicUrlPrefix}/users/current.json`;
|
||||||
|
|
||||||
|
const resp = await axios.get(url, {
|
||||||
|
headers: { 'X-Redmine-API-Key': redmineToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp && resp.data && resp.statusText == 'OK') {
|
||||||
|
const data = resp.data;
|
||||||
|
if (data.user.id == redmineId) {
|
||||||
|
const userData = await this.usersService.getUser(redmineId);
|
||||||
|
const userMetaInfo: UserMetaInfoModel = {
|
||||||
|
telegram_chat_id: msg.chat.id,
|
||||||
|
user_id: userData.id,
|
||||||
|
};
|
||||||
|
await this.userMetaInfoService.save(userMetaInfo);
|
||||||
|
return result(true, `Данные для ${userData.name} подтверждены`, {
|
||||||
|
redmineId: redmineId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result(false, 'Не удалось проверить подлинность указанных данных');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async leave(msg: TelegramBot.Message): Promise<void> {
|
||||||
|
const telegramChatId = msg.chat.id;
|
||||||
|
const userMetaInfo = await this.userMetaInfoService.findByTelegramId(
|
||||||
|
telegramChatId,
|
||||||
|
);
|
||||||
|
if (userMetaInfo) {
|
||||||
|
await this.userMetaInfoService.delete(userMetaInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/user-meta-info/user-meta-info.service.ts
Normal file
63
src/user-meta-info/user-meta-info.service.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { CacheTTL, Injectable } from '@nestjs/common';
|
||||||
|
import { UserMetaInfo } from 'src/couchdb-datasources/user-meta-info';
|
||||||
|
import { UserMetaInfoModel } from 'src/models/user-meta-info.model';
|
||||||
|
import nano from 'nano';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserMetaInfoService {
|
||||||
|
constructor(private userMetaInfo: UserMetaInfo) {}
|
||||||
|
|
||||||
|
@CacheTTL(60)
|
||||||
|
async findByRedmineId(id: number): Promise<UserMetaInfoModel | null> {
|
||||||
|
const db = await this.userMetaInfo.getDatasource();
|
||||||
|
try {
|
||||||
|
return await db.get(String(id));
|
||||||
|
} catch (ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CacheTTL(60)
|
||||||
|
async findByTelegramId(id: number): Promise<UserMetaInfoModel | null> {
|
||||||
|
const db = await this.userMetaInfo.getDatasource();
|
||||||
|
try {
|
||||||
|
const resp = await db.find({
|
||||||
|
selector: { telegram_chat_id: id },
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
return resp.docs && resp.docs[0] ? resp.docs[0] : null;
|
||||||
|
} catch (ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(data: UserMetaInfoModel): Promise<void> {
|
||||||
|
const id = String(data.user_id);
|
||||||
|
const db = await this.userMetaInfo.getDatasource();
|
||||||
|
let item: (nano.MaybeDocument & UserMetaInfoModel) | null = null;
|
||||||
|
const newItem: nano.MaybeDocument & UserMetaInfoModel = {
|
||||||
|
_id: id,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
item = await db.get(id);
|
||||||
|
} catch (ex) {}
|
||||||
|
if (item) {
|
||||||
|
newItem._id = item._id;
|
||||||
|
newItem._rev = item._rev;
|
||||||
|
}
|
||||||
|
await db.insert(newItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(data: UserMetaInfoModel): Promise<void> {
|
||||||
|
const id = String(data.user_id);
|
||||||
|
const db = await this.userMetaInfo.getDatasource();
|
||||||
|
let item: (nano.MaybeDocument & UserMetaInfoModel) | null = null;
|
||||||
|
try {
|
||||||
|
item = await db.get(id);
|
||||||
|
} catch (ex) {}
|
||||||
|
if (item) {
|
||||||
|
await db.destroy(item._id, item._rev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue