const {Telegraf, Markup} = require('telegraf');
const glpm = require('./lib/glpm.js');
const cns = require('./lib/const.js');
const {parseTickets, parseComments, refreshStatus, editTicketStatus} = require('./lib/glpiParse.js');
const {sleep, createThread, closeThread, getTicketColor, editMessageMarkup, editMessageText} = require('./lib/utils.js');
const fs = require('fs');
const dir = __dirname;
let ticketData = {};
let conf = JSON.parse(fs.readFileSync(dir + "/data/conf.json"));
const glpiUrl = conf.glpiConfig.apiurl.replace("apirest.php", "");
let messageData = JSON.parse(fs.readFileSync(dir + "/data/messageData.json"));
let configData = {};
const bot = new Telegraf(conf.telegramBotToken, {
handlerTimeout: 90000 * 5
});
bot.start(async (ctx) => {
ctx.reply('Добро пожаловать в чат-бот технической поддержки ' + conf.CompanyName, cns.keyboards.start);
await deleteMessage(ctx, ctx.message.message_id);
delete ticketData[ctx.chat.id];
});
bot.hears('Подать заявку', async (ctx) => {
if(ctx.chat.id != conf.supportChatId){
ticketData[ctx.chat.id] = {'navigate': [], 'flag': '', 'data': {'Автор заявки': '', 'Кабинет': ''}};
ticketData[ctx.chat.id]['chatId'] = ctx.message.chat.id;
await ctx.reply('Выберите, пожалуйста, наиболее подходящую категорию', cns.keyboards.main);
await deleteMessage(ctx, ctx.message.message_id);
}
});
// Настройка кнопок для переназначения заявки
bot.hears('/configurationUserGroups', async (ctx) => {
if(ctx.chat.id == conf.supportChatId){
await bot.telegram.sendMessage(conf.supportChatId, 'Выберите действие', {
parse_mode: 'HTML',
reply_markup: {inline_keyboard: cns.inlineKeyboards.configUserGroups}
});
await deleteMessage(ctx, ctx.message.message_id);
}
});
// Создаём кнопки основного меню
createButton('Принтеры', 'Категория', cns.keyboards.printers, cns.keyboards.main, 'Выберите проблему');
createButton('Замена картриджа', 'Проблема', cns.keyboards.colors, cns.keyboards.printers, 'Выберите цвет картриджа');
for(let i = 0; i < cns.colors.length; i++){
createButton(cns.colors[i], 'Цвет картриджа', cns.keyboards.printModels, cns.keyboards.colors, 'Выберите модель принтера');
}
for(let i = 0; i < cns.printers.length; i++){
createButton(cns.printers[i], 'Модель принтера', cns.keyboards.back, cns.keyboards.printModels, 'Укажите, пожалуйста, номер кабинета', "location", false);
}
createButton('Настройка принтера', 'Проблема', cns.keyboards.printModels, cns.keyboards.printers, 'Выберите модель принтера');
createButton('Другие проблемы', 'Проблема', cns.keyboards.printModels, cns.keyboards.printers, 'Выберите модель принтера');
createButton(
'Сброс пароля', 'Категория', cns.keyboards.back, cns.keyboards.main, 'Укажите, пожалуйста, номер кабинета',
"location", "Укажите программное обеспечение, от которого утерян пароль, и Ваш логин к нему (по возможности)"
);
createButton('Физические устройства', 'Категория', cns.keyboards.back, cns.keyboards.main, 'Укажите, пожалуйста, номер кабинета', "location", "Опишите Вашу проблему");
createButton('Программное обеспечение', 'Категория', cns.keyboards.applications, cns.keyboards.main, 'Выберите проблему');
createButton('Загрузка сайтов', 'Проблема', cns.keyboards.back, cns.keyboards.applications, 'Укажите, пожалуйста, номер кабинета', "location", "Опишите Вашу проблему");
createButton('Локальное ПО', 'Проблема', cns.keyboards.back, cns.keyboards.applications, 'Укажите, пожалуйста, номер кабинета', "location", "Опишите Вашу проблему");
createButton('Виртуальные машины', 'Проблема', cns.keyboards.back, cns.keyboards.applications, 'Укажите, пожалуйста, номер кабинета', "location", "Опишите Вашу проблему");
createButton('Сеть', 'Категория', cns.keyboards.back, cns.keyboards.main, 'Укажите, пожалуйста, номер кабинета', "location", "Опишите Вашу проблему");
// Общие команды для основного меню
bot.hears('Назад', async (ctx) => {
try{
if(ctx.chat.id != conf.supportChatId){
let keys = Object.keys(ticketData[ctx.chat.id]['data']);
let lastKey = keys[keys.length - 1];
delete ticketData[ctx.chat.id]['data'][lastKey];
await ctx.reply('Возвращаемся', ticketData[ctx.chat.id]['navigate'].pop());
await deleteMessage(ctx, ctx.message.message_id - 1);
await deleteMessage(ctx, ctx.message.message_id);
}
}catch{
ctx.reply('Добро пожаловать в чат-бот технической поддержки ' + conf.CompanyName, cns.keyboards.start);
}
});
bot.hears('Отменить заявку', async (ctx) => {
if(ctx.chat.id != conf.supportChatId){
delete ticketData[ctx.chat.id];
await ctx.reply('Заявка отменена', Markup.keyboard([['Подать заявку']]).resize());
await deleteMessage(ctx, ctx.message.message_id - 1);
await deleteMessage(ctx, ctx.message.message_id);
}
});
bot.hears('Отправить заявку', async (ctx) => {
if(ctx.chat.id != conf.supportChatId && ticketData.hasOwnProperty(ctx.chat.id) && ticketData[ctx.chat.id].data["Кабинет"].length > 1){
if(!ctx.chat.last_name) ctx.chat.last_name = '';
ticketData[ctx.chat.id]['data']['Автор заявки'] = ctx.chat.first_name + ' ' + ctx.chat.last_name;
let title = ticketData[ctx.chat.id].
data["Кабинет"] + ' - ' + ticketData[ctx.chat.id].
data["Категория"] + ' - ' + ticketData[ctx.chat.id].
data["Автор заявки"];
let textToGLPI = '';
for(key in ticketData[ctx.chat.id]['data']){
textToGLPI += '' + key + ': ' + ticketData[ctx.chat.id]['data'][key].replace(/[<>/]/g, '') + '
';
}
let ticketId = await glpm.createTicket(title, textToGLPI);
let messageText = `🟢 ЗАЯВКА №${ticketId}\n\n`;
let userLogin = '';
if(ctx.chat.username) userLogin = ' (@' + ctx.chat.username + ')';
ticketData[ctx.chat.id]['data']['Автор заявки'] += userLogin;
for(key in ticketData[ctx.chat.id]['data']){
messageText += '' + key + ': ' + ticketData[ctx.chat.id]['data'][key].replace(/[<>/]/g, '') + '\n';
}
let messg = await ctx.telegram.sendMessage(conf.supportChatId, messageText, {
parse_mode: 'HTML',
reply_markup: {inline_keyboard: cns.inlineKeyboards.open}
});
messageData.data[ticketId] = {
messageId: messg.message_id,
userMassageId: ctx.message.message_id + 2,
userChatId: ctx.chat.id,
status: 1
}
let threadTitle = `🟢 ${ticketId}${title.replace(ticketData[ctx.chat.id].data["Кабинет"], '')}`;
await createThread(bot, messageData, ticketId, threadTitle);
await ctx.reply('Заявка отправлена', cns.keyboards.start);
await deleteMessage(ctx, ctx.message.message_id - 1);
await deleteMessage(ctx, ctx.message.message_id);
await ctx.telegram.sendMessage(ctx.chat.id, messageText, {
parse_mode: 'HTML',
reply_markup: {inline_keyboard: cns.inlineKeyboards.userAddComment}
});
fs.writeFileSync(dir + "/data/messageData.json", JSON.stringify(messageData, null, 3));
delete ticketData[ctx.chat.id];
}
});
// Обработка сообщений
bot.on('text', async (ctx) => {
if(ctx.chat.id != conf.supportChatId){
let ticketId;
try{
ticketId = ctx.message.reply_to_message.text.split('№')[1].split('\n')[0];
}catch{}
if(!ticketData.hasOwnProperty(ctx.chat.id) && ticketId){
if(!messageData.data[ticketId].hasOwnProperty('threadId')){
await ctx.reply("Эта заявка уже закрыта. Создайте, пожалуйста, новую");
return;
}
if(!ctx.chat.last_name) ctx.chat.last_name = '';
let userName = ctx.chat.first_name + ' ' + ctx.chat.last_name;
let messageText = `Комментарий от ${userName}:\n\n${ctx.message.text}`;
await bot.telegram.sendMessage(conf.supportChatId, messageText, {
parse_mode: "HTML", message_thread_id: messageData.data[ticketId].threadId
});
let discript = `Комментарий от ${userName}:
`;
await glpm.addComment(ticketId, discript + ctx.message.text);
}else if(ticketId){
await ctx.reply("Чтобы отправить комментарий, завершите текущую заявку, отмените её или перезапустите бота");
return;
}else if(ticketData[ctx.chat.id] && ticketData[ctx.chat.id]["flag"] == 'location'){
ticketData[ctx.chat.id]['data']['Кабинет'] = ctx.message.text;
if(ticketData[ctx.chat.id]["nextText"]){
await ctx.reply(ticketData[ctx.chat.id]["nextText"]);
ticketData[ctx.chat.id]["flag"] = 'description';
await deleteMessage(ctx, ctx.message.message_id - 1);
await deleteMessage(ctx, ctx.message.message_id);
}else{
await ctx.reply('Подтвердите отправку', cns.keyboards.final);
await deleteMessage(ctx, ctx.message.message_id - 1);
await deleteMessage(ctx, ctx.message.message_id);
}
}else if(ticketData[ctx.chat.id] && ticketData[ctx.chat.id]["flag"] == 'description'){
ticketData[ctx.chat.id]['data']['Описание'] = ctx.message.text;
await ctx.reply('Подтвердите отправку', cns.keyboards.final);
await deleteMessage(ctx, ctx.message.message_id - 1);
await deleteMessage(ctx, ctx.message.message_id);
}
}else if(ctx.message.message_thread_id){
let ticket;
for(let i in messageData.data){
if(messageData.data[i].threadId == ctx.message.message_thread_id){
ticket = i;
break;
}
}
if(messageData.data[ticket].hasOwnProperty('userChatId')){
try{
await bot.telegram.sendMessage(messageData.data[ticket].userChatId, ctx.message.text, {
reply_parameters: {message_id: messageData.data[ticket].userMassageId}
});
}catch{
await bot.telegram.sendMessage(messageData.data[ticket].userChatId, ctx.message.text);
}
}
await glpm.addComment(ticket, ctx.message.text);
}else if(configData.hasOwnProperty("flag")){
configData["text"] = ctx.message.text.trim();
configData["id"] = ctx.message.message_id;
}
});
// Обработка изображений из приватных чатов
bot.on('photo', async (ctx) => {
if(ctx.chat.id != conf.supportChatId){
let ticketId;
let messg = ctx.message.reply_to_message.text;
try{
ticketId = messg.split('№')[1].split('\n')[0];
}catch{}
if(!ticketData.hasOwnProperty(ctx.chat.id) && ticketId){
if(!messageData.data[ticketId].hasOwnProperty(threadId)){
let ticket = await glpm.getItem('Ticket', ticketId);
let title = `🟢 ${ticketId}${ticket.name.split('-')[1]}-${ticket.name.split('-')[2]}`;
await createThread(bot, messageData, ticketId, title);
}
await bot.telegram.sendPhoto(conf.supportChatId, ctx.message.photo[0].file_id, {message_thread_id: messageData.data[ticketId]["threadId"]});
}
}
});
// Сохраняем ID последнего сообщения в чате
bot.on('message', async (ctx) => {
if(ctx.message.forum_topic_edited){
await deleteMessage(ctx, ctx.message.message_id);
}
});
// Функция для создания кнопок основного меню с похожим функционалом
function createButton(buttonName, param, keyboard, navKeyboard, messg, flag, nextText){
bot.hears(buttonName, async (ctx) => {
if(ctx.chat.id != conf.supportChatId && ticketData.hasOwnProperty(ctx.chat.id)){
ticketData[ctx.chat.id]['navigate'].push(navKeyboard);
ticketData[ctx.chat.id]['data'][param] = buttonName;
await ctx.reply(messg, keyboard);
await deleteMessage(ctx, ctx.message.message_id - 1);
await deleteMessage(ctx, ctx.message.message_id);
if(flag){
ticketData[ctx.chat.id]["flag"] = flag;
ticketData[ctx.chat.id]["nextText"] = nextText;
}
}else{
ctx.reply('Добро пожаловать в чат-бот технической поддержки ' + conf.CompanyName, cns.keyboards.start);
}
});
}
// Вспомогательные функции
async function deleteMessage(ctx, message_id){
try{
await bot.telegram.deleteMessage(ctx.message.chat.id, message_id);
}catch{}
}
// Создаём обработчики инлайн кнопок
createAction('OpenTicket', cns.inlineKeyboards.confirmOpen);
createAction('CloseTicket', cns.inlineKeyboards.confirmClose);
createAction('ChangeStatus', cns.inlineKeyboards.changeStatus);
createAction('ConfirmOpen', cns.inlineKeyboards.open, 1);
createAction('ConfirmClose', cns.inlineKeyboards.close, 6);
createAction('WaitingStatus', cns.inlineKeyboards.open, 4);
createAction('WorkingStatus', cns.inlineKeyboards.open, 2);
createAction('OpenThread', cns.inlineKeyboards.open);
function createAction(action, keyboard, status){
try{
bot.action(action, async (ctx) => {
let message = ctx.update.callback_query.message;
let ticketId = message.text.split('\n')[0].split('№')[1];
let td = messageData.data[ticketId];
if(status || action == 'OpenThread'){
await glpm.changeStatusTicket(ticketId, status);
await editTicketStatus(bot, messageData, ticketId);
if(status == 6){
if(td.hasOwnProperty('userChatId')){
try{
await bot.telegram.sendMessage(td.userChatId, "Заявка закрыта", {
reply_parameters: {message_id: td.userMassageId},
parse_mode: "HTML"
});
}catch{
await bot.telegram.sendMessage(td.userChatId, "Заявка закрыта", {
parse_mode: "HTML"
});
}
}
if(td.hasOwnProperty('threadId')){
await closeThread(bot, messageData, ticketId);
}
}else if(!td.hasOwnProperty('threadId') && (status == 1 || action == 'OpenThread')){
let color = await getTicketColor(status || td.status);
let title;
if(td.hasOwnProperty('userChatId')){
let problem = message.text.split('\n')[4].replace('Категория: ', '');
let author = message.text.split('\n')[2].replace('Автор заявки: ', '').replace(/\(@[^)]+\)/g, '');
title = `${color} ${ticketId} - ${problem} - ${author}`;
}else{
title = `${color} ${ticketId} - ${message.text.split('\n')[3].replace('Проблема: ', '')}`;
}
await createThread(bot, messageData, ticketId, title);
}
}else{
await editMessageMarkup(bot, message.message_id, keyboard);
}
});
}catch(e){
fs.appendFileSync(dir + "/logs/logs.txt", e.stack + "\n\n");
}
}
// Обработчик для кнопки "Отмена"
bot.action('RefreshTicket', async (ctx) => {
let message = ctx.update.callback_query.message;
let ticketId = message.text.split('№')[1].split('\n')[0];
await editTicketStatus(bot, messageData, ticketId);
});
// Обработчик для кнопки с подсказкой о добавлении комментария
bot.action('UserAddComment', async (ctx) => {
await ctx.reply('Чтобы отправить комментарий, ответьте на сообщение с номером заявки (которое начинается с 🟢)', {parse_mode: "HTML"});
});
// Изменить конфигурацию пользовательских групп
createConfigButtons('AddNewGroup', 'Укажите название новой группы');
createConfigButtons('AddNewUser', 'Укажите группу и glpi id пользователя в формате:\n[Группа]: [id]');
createConfigButtons('RemoveGroup', 'Укажите название удаляемой группы');
createConfigButtons('RemoveUser', 'Укажите группу и glpi id пользователя в формате:\n[Группа]: [id]');
function createConfigButtons(action, messageText){
bot.action(action, async (ctx) => {
let message = ctx.update.callback_query.message;
await editMessageText(bot, message.message_id, messageText, cns.inlineKeyboards.confirmConfig)
configData["flag"] = action;
});
}
bot.action('ConfirmConfig', async (ctx) => {
if(configData.hasOwnProperty('id')){
let messageId = ctx.update.callback_query.message.message_id;
switch(configData.flag){
case "AddNewGroup": conf.userGroups[configData.text] = []; break;
case "AddNewUser": let gu = configData.text.split(':'); conf.userGroups[gu[0].trim()].push(gu[1].trim()); break;
case "RemoveGroup": delete conf.userGroups[configData.text]; break;
case "RemoveUser": let rgu = configData.text.split(':'); let index = conf.userGroups[rgu[0].trim()].indexOf(rgu[1].trim());
index >= 0 ? conf.userGroups[rgu[0].trim()].splice(index, 1) : false; break;
}
let jsonData = JSON.stringify(conf, null, 3);
fs.writeFileSync(dir + "/data/conf.json", jsonData);
createAssignActions();
await deleteMessage(conf.supportChatId, configData.id);
configData = {};
await editMessageMarkup(bot, messageId, cns.inlineKeyboards.configUserGroups);
}
});
bot.action('ExitConfig', async (ctx) => {
configData = {};
let message = ctx.update.callback_query.message;
await bot.telegram.deleteMessage(conf.supportChatId, message.message_id);
});
bot.action('CancellConfirm', async (ctx) => {
await deleteMessage(ctx.update.callback_query, configData.id);
configData = {};
let message = ctx.update.callback_query.message;
await editMessageMarkup(bot, message.message_id, cns.inlineKeyboards.configUserGroups);
});
// Обработчики для кнопок с переназначением заявок
bot.action('AssignTicket', async (ctx) => {
let message = ctx.update.callback_query.message;
let keyboard = [[]];
let row = 0;
for(let key in conf.userGroups){
if(row == 3){
keyboard.push([]);
row = 0;
}
keyboard[keyboard.length-1].push({text: key, callback_data: 'ButtonFor_' + key});
row++;
}
keyboard[keyboard.length-1].push({text: 'Отмена', callback_data: 'RefreshTicket'});
await editMessageMarkup(bot, message.message_id, keyboard);
});
// Создание обработчиков событий для переназначения заявок
createAssignActions();
function createAssignActions(){
for(let key in conf.userGroups){
bot.action('ButtonFor_' + key, async (ctx) => {
let message = ctx.update.callback_query.message;
let ticketId = message.text.split('№')[1].split('\n')[0];
for(let i in conf.userGroups[key]){
await glpm.assignTicket(ticketId, conf.userGroups[key][i]);
}
if(message.text.indexOf("⚫") < 0){
await glpm.changeStatusTicket(ticketId, 2);
}
await editTicketStatus(bot, messageData, ticketId);
if(messageData.data[ticketId].hasOwnProperty('threadId')){
await closeThread(bot, messageData, ticketId);
}
});
}
}
process.on('uncaughtException', (error) => {
console.log(error);
});
bot.launch();
// Сборщик заявок и комментариев из GLPI
(async () => {
let counter = 0; // счетчик для выполнения функции refreshStatus()
while (true) {
try{
await parseTickets(bot, messageData);
await parseComments(bot, messageData);
if(counter >= 60){
await refreshStatus(bot, messageData);
counter = 0;
}
}catch(e){
fs.appendFileSync(dir + "/logs/logs.txt", e.stack + "\n\n");
}
await sleep(10000);
counter++;
}
})();