Переделан сервис интеграции с telegram через простую либу с командами
This commit is contained in:
parent
908b927bca
commit
3a869023e5
5 changed files with 1798 additions and 250 deletions
1851
package-lock.json
generated
1851
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -33,13 +33,12 @@
|
||||||
"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",
|
||||||
"nestjs-telegraf": "^2.6.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",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.2.0",
|
||||||
"socket.io": "^4.4.1",
|
"socket.io": "^4.4.1"
|
||||||
"telegraf": "^4.8.6"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^8.0.0",
|
"@nestjs/cli": "^8.0.0",
|
||||||
|
|
@ -49,6 +48,7 @@
|
||||||
"@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",
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import { ChangesCacheWriterService } from './changes-cache-writer/changes-cache-
|
||||||
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 { TelegramBotService } from './telegram-bot/telegram-bot.service';
|
||||||
import { TelegrafModule } from 'nestjs-telegraf';
|
|
||||||
import { UserMetaInfoService } from './user-meta-info/user-meta-info.service';
|
import { UserMetaInfoService } from './user-meta-info/user-meta-info.service';
|
||||||
import { UserMetaInfo } from './couchdb-datasources/user-meta-info';
|
import { UserMetaInfo } from './couchdb-datasources/user-meta-info';
|
||||||
|
|
||||||
|
|
@ -29,9 +28,6 @@ import { UserMetaInfo } from './couchdb-datasources/user-meta-info';
|
||||||
config: configuration().redmineIssueEventEmitterConfig,
|
config: configuration().redmineIssueEventEmitterConfig,
|
||||||
}),
|
}),
|
||||||
ConfigModule.forRoot({ load: [configuration] }),
|
ConfigModule.forRoot({ load: [configuration] }),
|
||||||
TelegrafModule.forRoot({
|
|
||||||
token: configuration().telegramBotToken,
|
|
||||||
}),
|
|
||||||
CacheModule.register({
|
CacheModule.register({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
}),
|
}),
|
||||||
|
|
@ -63,6 +59,7 @@ 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,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,153 @@
|
||||||
import { UsersService } from '@app/event-emitter/users/users.service';
|
import { UsersService } from '@app/event-emitter/users/users.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Update, Ctx, Start, Help, On, Hears } from 'nestjs-telegraf';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import TelegramBot from 'node-telegram-bot-api';
|
||||||
import { UserMetaInfoService } from 'src/user-meta-info/user-meta-info.service';
|
import { UserMetaInfoService } from 'src/user-meta-info/user-meta-info.service';
|
||||||
import { Context } from 'telegraf';
|
import axios from 'axios';
|
||||||
|
import { UserMetaInfoModel } from 'src/models/user-meta-info.model';
|
||||||
|
|
||||||
@Update()
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TelegramBotService {
|
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(
|
constructor(
|
||||||
private userMetaInfoService: UserMetaInfoService,
|
private userMetaInfoService: UserMetaInfoService,
|
||||||
private usersService: UsersService,
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
@Start()
|
private async initTelegramBot(): Promise<void> {
|
||||||
async start(@Ctx() ctx: Context): Promise<void> {
|
const Telegram = await require('node-telegram-bot-api');
|
||||||
const chat = await ctx.getChat();
|
this.bot = new Telegram(this.telegramBotToken, { polling: true });
|
||||||
const chatId = chat.id;
|
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(
|
const userMetaInfo = await this.userMetaInfoService.findByTelegramId(
|
||||||
chatId,
|
msg.chat.id,
|
||||||
);
|
);
|
||||||
if (!userMetaInfo) {
|
let helpMessage: string;
|
||||||
await ctx.reply('Привет, незнакомец!');
|
if (userMetaInfo) {
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
helpMessage = [
|
||||||
|
`/current_issues - мои текущие задачи`,
|
||||||
|
`/help`
|
||||||
|
].join('\n');
|
||||||
} else {
|
} else {
|
||||||
const user = await this.usersService.getUser(userMetaInfo.user_id);
|
// eslint-disable-next-line prettier/prettier
|
||||||
await ctx.reply(`Привет, ${user.name}`);
|
helpMessage = [
|
||||||
|
`/register <redmine_id> <redmine_token>`,
|
||||||
|
`/help`
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
this.bot.sendMessage(msg.chat.id, helpMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessageByRedmineId(
|
||||||
|
redmineId: number,
|
||||||
|
msg: string,
|
||||||
|
): 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);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessageByName(
|
||||||
|
firstname: string,
|
||||||
|
lastname: string,
|
||||||
|
msg: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const user = await this.usersService.findUserByName(firstname, lastname);
|
||||||
|
if (!user) return false;
|
||||||
|
return await this.sendMessageByRedmineId(user.id, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Help()
|
return result(false, 'Не удалось проверить подлинность указанных данных');
|
||||||
async help(@Ctx() ctx: Context): Promise<void> {
|
|
||||||
await ctx.reply('Send me a sticker');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@On('sticker')
|
private async leave(msg: TelegramBot.Message): Promise<void> {
|
||||||
async on(@Ctx() ctx: Context): Promise<void> {
|
const telegramChatId = msg.chat.id;
|
||||||
await ctx.reply('👍');
|
const userMetaInfo = await this.userMetaInfoService.findByTelegramId(
|
||||||
|
telegramChatId,
|
||||||
|
);
|
||||||
|
if (userMetaInfo) {
|
||||||
|
await this.userMetaInfoService.delete(userMetaInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Hears('мои задачи')
|
|
||||||
async hears(@Ctx() ctx: Context): Promise<void> {
|
|
||||||
await ctx.reply('В разработке');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export class UserMetaInfoService {
|
||||||
constructor(private userMetaInfo: UserMetaInfo) {}
|
constructor(private userMetaInfo: UserMetaInfo) {}
|
||||||
|
|
||||||
@CacheTTL(60)
|
@CacheTTL(60)
|
||||||
async findById(id: number): Promise<UserMetaInfoModel | null> {
|
async findByRedmineId(id: number): Promise<UserMetaInfoModel | null> {
|
||||||
const db = await this.userMetaInfo.getDatasource();
|
const db = await this.userMetaInfo.getDatasource();
|
||||||
try {
|
try {
|
||||||
return await db.get(String(id));
|
return await db.get(String(id));
|
||||||
|
|
@ -35,7 +35,10 @@ export class UserMetaInfoService {
|
||||||
const id = String(data.user_id);
|
const id = String(data.user_id);
|
||||||
const db = await this.userMetaInfo.getDatasource();
|
const db = await this.userMetaInfo.getDatasource();
|
||||||
let item: (nano.MaybeDocument & UserMetaInfoModel) | null = null;
|
let item: (nano.MaybeDocument & UserMetaInfoModel) | null = null;
|
||||||
const newItem: nano.MaybeDocument & UserMetaInfoModel = { ...data };
|
const newItem: nano.MaybeDocument & UserMetaInfoModel = {
|
||||||
|
_id: id,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
item = await db.get(id);
|
item = await db.get(id);
|
||||||
} catch (ex) {}
|
} catch (ex) {}
|
||||||
|
|
@ -45,4 +48,16 @@ export class UserMetaInfoService {
|
||||||
}
|
}
|
||||||
await db.insert(newItem);
|
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