Telegram-glpi 3.0

This commit is contained in:
atorop
2024-08-24 19:41:02 +03:00
parent 0f03f85511
commit 6042849061
23 changed files with 1907 additions and 447 deletions

View File

@@ -1,21 +1,19 @@
1. Клонировать репозиторий 1. Клонировать репозиторий
2. Получить токен телеграм бота с помощью botFather
3. Создать телеграм-канал, включить в его настройках темы
4. Добавить бота в администраторы канала и разрешить ему управление темами, остальным юзерам запретить
5. В файле data/conf.json заменить параметры:
- telegramBotToken: токен телеграм бота 2. В файле data/conf.json заменить параметры:
- supportChatId: id телеграм канала
- CompanyName: название компании (для приветствия)
- glpiConfig:
- apiurl: "http://[имя домена]/apirest.php"
- app_token: это токен приложения, настраивается в админке
- user_token: это "app-token" в настройках юзера
- user_id: id юзера, через которого будет авторизироваться бот (видно в адресной строке)
6. Проверить в telegram_support.service путь к исполняемому файлу и добавить его в папку /etc/systemd/system/ (для debian) telegramBotToken: токен телеграм бота
7. Для работы бота должен быть установлен node.js (все остальные зависимости находятся в папке node_modules) supportChatId: id чата для техподдержки
8. Обновить демоны командой: CompanyName: название компании (для приветствия)
glpiConfig:
apiurl: "http://[имя домена]/apirest.php"
app_token: это токен приложения, настраивается в админке
user_token: это "app-token" в настройках юзера
user_id: id юзера, через которого будет авторизироваться бот (видно в адресной строке)
3. Проверить в telegram_support.service путь к исполняемому файлу и добавить его в папку /etc/systemd/system/ (для debian)
4. Для работы бота должен быть установлен node.js (все остальные зависимости находятся в папке node_modules)
5. Обновить демоны командой:
> systemctl daemon-reload > systemctl daemon-reload
@@ -26,10 +24,17 @@
**Создание образа и запуск его в docker-контейнере** **Создание образа и запуск его в docker-контейнере**
1. Выполнить первые 5 пунктов из инструкции выше 1. Выполнить первые 2 пункта из инструкции выше
2. Перейти в папку telegram-support-bot 2. Перейти в папку telegram-support-bot
3. Запустить контейнер командой: 3. Запустить контейнер командой:
> docker compose up -d --build > docker compose up -d --build
**Изменение пользовательских групп**
1. Чтобы добавить свои группы пользователей (это нужно для переназначения заявок в системе glpi прямо из телеграма), нужно ввести команду /configurationUserGroups в главной ветке канала для техподдержки и следовать инструкции в ответном сообщении

View File

@@ -1,11 +1,27 @@
{ {
"telegramBotToken": "", "telegramBotToken": "",
"supportChatId": "", "supportChatId": "",
"TesttelegramBotToken": "",
"TestsupportChatId": "",
"CompanyName": "", "CompanyName": "",
"glpiConfig": { "glpiConfig": {
"apiurl": "", "apiurl": "",
"app_token": "", "app_token": "",
"user_token": "", "user_token": "",
"user_id": "" "user_id": 5
},
"userGroups": {
"Почта": [
"115",
"125"
],
"Хостинг": [
"94",
"169"
],
"Телефония": [
"52",
"67"
]
} }
} }

View File

@@ -1,7 +1,7 @@
{ {
"ticket": 0, "ticket": 0,
"comment": 0, "comment": 0,
"history": { "data": {
} }
} }

View File

@@ -1,3 +0,0 @@
{
}

View File

@@ -11,3 +11,4 @@ services:
volumes: volumes:
data: data:

View File

@@ -28,11 +28,16 @@ exports.keyboards = {
}; };
exports.inlineKeyboards = { exports.inlineKeyboards = {
open: [[{text: '✅', callback_data: 'CloseTicket'}, {text: '💬', callback_data: 'AddComment'}]], open: [[{text: '✅', callback_data: 'CloseTicket'}, {text: '➡️', callback_data: 'AssignTicket'}, {text: '*️⃣', callback_data: 'ChangeStatus'}]],
close: [[{text: '✔', callback_data: 'OpenTicket'}, {text: '💬', callback_data: 'AddComment'}]], close: [[{text: '✔', callback_data: 'OpenTicket'}, {text: '➡️', callback_data: 'AssignTicket'}, {text: '*️⃣', callback_data: 'ChangeStatus'}]],
confirmOpen: [[{text: 'Открыть заявку', callback_data: 'ConfirmOpen'}, {text: 'Отмена', callback_data: 'RefreshStatus'}]], confirmOpen: [[{text: 'Открыть заявку', callback_data: 'ConfirmOpen'}, {text: 'Отмена', callback_data: 'RefreshStatus'}]],
confirmClose: [[{text: 'Закрыть заявку', callback_data: 'ConfirmClose'}, {text: 'Отмена', callback_data: 'RefreshStatus'}]], confirmClose: [[{text: 'Закрыть заявку', callback_data: 'ConfirmClose'}, {text: 'Отмена', callback_data: 'RefreshStatus'}]],
userAddComment: [[{text: '❓ Добавить комментарий', callback_data: 'UserAddComment'}]], userAddComment: [[{text: '❓ Добавить комментарий', callback_data: 'UserAddComment'}]],
threadPin: [[{text: 'Закрыть тему', callback_data: 'CloseThread'}]], changeStatus: [[{text: 'В ожидание', callback_data: 'WaitingStatus'}, {text: 'В работу', callback_data: 'WorkingStatus'}], [{text: 'Открыть тему', callback_data: 'OpenThread'}, {text: 'Отмена', callback_data: 'RefreshStatus'}]],
threadPinConfirm: [[{text: 'Подтвердить', callback_data: 'ConfirmCloseThread'}, {text: 'Отмена', callback_data: 'CancelCloseThread'}]] configUserGroups: [
[{text: 'Add new group', callback_data: 'AddNewGroup'}, {text: 'Add new user in group', callback_data: 'AddNewUser'}],
[{text: 'Remove group', callback_data: 'RemoveGroup'}, {text: 'Remove user', callback_data: 'RemoveUser'}, {text: 'Exit', callback_data: 'ExitConfig'}]
],
confirmConfig: [[{text: 'Confirm', callback_data: 'ConfirmConfig'}, {text: 'Cancell', callback_data: 'CancellConfirm'}]]
}; };

160
lib/glpiParse.js Normal file
View File

@@ -0,0 +1,160 @@
const glpm = require('./glpm.js');
const cns = require('./const.js');
const {
sleep, htmlToText, createThread, closeThread, getTicketColor,
parseMessageText, getKeyboardFromStatus, editMessageText, editMessageMarkup
} = require('./utils.js');
const fs = require('fs');
const dir = __dirname;
let conf = JSON.parse(fs.readFileSync(dir + "/../data/conf.json"));
const glpiUrl = conf.glpiConfig.apiurl.replace("apirest.php", "");
exports.parseTickets = async (bot, messageData) => {
let listTickets = await glpm.getAllItems('Ticket', 5);
for(let i = 4; i >= 0; i--){
let ticketId;
if(!listTickets[i]) break;
ticketId = listTickets[i].id;
if(ticketId <= messageData.ticket) continue;
if(listTickets[i].users_id_recipient != conf.glpiConfig.user_id){
let usersArray = await glpm.getUsers(ticketId);
let authorEmail;
if(!usersArray[0]) continue;
if(usersArray[0].hasOwnProperty('alternative_email') && usersArray[0].alternative_email){
authorEmail = usersArray[0].alternative_email;
}else{
let temp = await glpm.getItem("User", usersArray[0].users_id);
authorEmail = temp.firstname + ' ' + temp.realname;
}
let text = await htmlToText(listTickets[i].content);
let messageText = `🟢 <b>ЗАЯВКА <a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">№${ticketId}</a></b>\n\n`;
messageText += `<b>Автор заявки: </b>${authorEmail}\n`;
messageText += `<b>Проблема: </b>${listTickets[i].name}\n<b>Описание: </b>`;
messageText += text;
if(messageText.length > 600){
messageText = `${messageText.substring(0, 500)} + '\n\n<b><a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">Читать дальше</a></b>`;
}
let messg = await bot.telegram.sendMessage(conf.supportChatId, messageText, {
parse_mode: 'HTML',
reply_markup: { inline_keyboard: cns.inlineKeyboards.open }
});
await editMessageText(bot, messg.message_id, messageText, cns.inlineKeyboards.open);
messageData.data[ticketId] = {
messageId: messg.message_id,
status: 1
};
let title = `🟢 ${ticketId} - ${listTickets[i].name}`;
await createThread(bot, messageData, ticketId, title);
await sleep(1000);
}
messageData.ticket = ticketId;
}
fs.writeFileSync(dir + "/../data/messageData.json", JSON.stringify(messageData, null, 3));
}
exports.parseComments = async (bot, messageData) => {
let listComments = await glpm.getAllItems('ITILFollowup', 5);
for (let i = 4; i >= 0; i--) {
if(!listComments[i]) break;
let commentId = listComments[i].id;
if(commentId <= messageData.comment) continue;
if(listComments[i].users_id != conf.glpiConfig.user_id){
let ticketId = listComments[i].items_id;
if(!messageData.data.hasOwnProperty(ticketId) || !messageData.data[ticketId].hasOwnProperty("threadId")) continue;
let comment = await htmlToText(listComments[i].content);
let user;
if(listComments[i].users_id){
let temp = await glpm.getItem("User", listComments[i].users_id);
user = temp.firstname + ' ' + temp.realname;
}else{
let temp = await glpm.getUsers(ticketId);
user = temp[0].alternative_email;
}
if(!messageData.data.hasOwnProperty(ticketId) || !messageData.data[ticketId].hasOwnProperty('threadId')){
return;
}
let messageText = `<b>Комментарий от ${user}:</b>\n\n${comment}`;
if(messageText.length > 2400){
messageText = `${messageText.substring(0, 2400)} + '\n\n<b><a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">Читать дальше</a></b>`;
}
await bot.telegram.sendMessage(conf.supportChatId, messageText, {parse_mode: "HTML", message_thread_id: messageData.data[ticketId].threadId});
await sleep(1000);
}
messageData.comment = commentId;
fs.writeFileSync(dir + "/../data/messageData.json", JSON.stringify(messageData, null, 3));
}
}
exports.refreshStatus = async (bot, messageData) => {
let listTickets = await glpm.getAllItems('Ticket', 100);
for(let i = 99; i >= 0; i--){
let ticketId = listTickets[i].id;
if(!listTickets[i] || !messageData.data.hasOwnProperty(ticketId)) continue;
let td = messageData.data[ticketId];
try{
if(td.status != listTickets[i].status){
let usersArray = await glpm.getUsers(ticketId);
let message = await htmlToText(listTickets[i].content);
let authorEmail;
if(usersArray[0].hasOwnProperty('alternative_email') && usersArray[0].alternative_email){
authorEmail = usersArray[0].alternative_email;
}else if(usersArray[0].users_id == conf.glpiConfig.user_id){
await editTicketStatus(bot, messageData, message);
break;
}else{
let temp = await glpm.getItem("User", usersArray[0].users_id);
authorEmail = temp.firstname + ' ' + temp.realname;
}
let color = await getTicketColor(listTickets[i].status);
let messageText = `${color} <b>ЗАЯВКА <a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">№${ticketId}</a></b>\n\n`;
messageText += `<b>Автор заявки: </b>${authorEmail}\n`;
messageText += `<b>Проблема: </b>${listTickets[i].name}\n<b>Описание: </b>`;
messageText += message;
if(messageText.length > 600){
messageText = `${messageText.substring(0, 500)} + '\n\n<b><a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">Читать дальше</a></b>`;
}
let inKeyboard = await getKeyboardFromStatus(listTickets[i].status);
if(td.hasOwnProperty('threadId')){
if(listTickets[i].status == 5 || listTickets[i].status == 6){
await closeThread(bot, messageData, ticketId);
}else{
let title = `${color} ${ticketId} - ${listTickets[i].name}`;
await bot.telegram.editForumTopic(conf.supportChatId, td.threadId, { name: title });
}
}
await editMessageText(bot, td.messageId, messageText, inKeyboard);
await editMessageText(bot, td.pinMessageId, messageText, inKeyboard);
messageData.data[ticketId].status = listTickets[i].status;
}else if(td.hasOwnProperty('threadId') && (listTickets[i].status == 5 || listTickets[i].status == 6)){
await closeThread(bot, messageData, ticketId);
}
}catch(e){
fs.appendFileSync(dir + "/../logs/logs.json", JSON.stringify("refreshStatus function: " + e, null, 3));
}
}
fs.writeFileSync(dir + "/../data/messageData.json", JSON.stringify(messageData, null, 3));
}
exports.editTicketStatus = async (bot, messageData, message) => {
let ticketId = message.text.split('№')[1].split('\n')[0];
let ticket = await glpm.getItem('Ticket', ticketId);
let inKeyboard = await getKeyboardFromStatus(ticket.status);
if(messageData.data[ticketId].status != ticket.status){
messageData.data[ticketId].status = ticket.status;
let color = await getTicketColor(ticket.status);
let title = ticket.name;
if(messageData.data[ticketId].hasOwnProperty('userChatId')){
title = `${title.split(' - ')[1]} - ${title.split(' - ')[2]}`;
}
title = `${color} ${ticketId} - ${title}`;
if(messageData.data[ticketId].hasOwnProperty('threadId')){
await bot.telegram.editForumTopic(conf.supportChatId, messageData.data[ticketId].threadId, { name: title });
}
let messageText = await parseMessageText(message, messageData, ticketId);
await editMessageText(bot, messageData.data[ticketId].messageId, messageText, inKeyboard);
await editMessageText(bot, messageData.data[ticketId].pinMessageId, messageText, inKeyboard);
}
await editMessageMarkup(bot, message.message_id, inKeyboard);
fs.writeFileSync(dir + "/../data/messageData.json", JSON.stringify(messageData, null, 3));
}

View File

@@ -25,8 +25,8 @@ exports.createTicket = async (title, description) => {
}) })
}); });
return response.data.id; return response.data.id;
}catch(error){ }catch(e){
console.error('Failed to create ticket:', error); fs.appendFileSync(__dirname + "/../logs/logs.json", JSON.stringify(e, null, 3));
} }
} }
@@ -50,8 +50,34 @@ exports.changeStatusTicket = async (ticketId, statusId) => {
}) })
}); });
return response; return response;
}catch(error){ }catch(e){
console.error('Failed to create ticket:', error); fs.appendFileSync(__dirname + "/../logs/logs.json", JSON.stringify(e, null, 3));
}
}
exports.assignTicket = async (ticketId, userId) => {
try{
const session = await glpi.initSession();
let token = session.data.session_token;
const response = await axios(conf.glpiConfig.apiurl + '/Ticket_User', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'App-Token': conf.glpiConfig.app_token,
'Authorization': conf.glpiConfig.user_token,
'Session-Token': token
},
data: JSON.stringify({
input: {
tickets_id: ticketId,
users_id: userId,
type: 2
}
})
});
return response.data;
}catch(e){
fs.appendFileSync(__dirname + "/../logs/logs.json", JSON.stringify(e, null, 3));
} }
} }
@@ -69,8 +95,8 @@ exports.getItem = async(item, id) => { // ITILFollowup = комментари
} }
}); });
return response.data; return response.data;
}catch(error){ }catch(e){
return 0; fs.appendFileSync(__dirname + "/../logs/logs.json", JSON.stringify(e, null, 3));
} }
} }
@@ -88,12 +114,13 @@ exports.getAllItems = async(item, cnt) => {
} }
}); });
return response.data; return response.data;
}catch(error){ }catch(e){
return error; fs.appendFileSync(__dirname + "/../logs/logs.json", JSON.stringify(e, null, 3));
} }
} }
exports.addComment = async(ticketId, comment) => { exports.addComment = async(ticketId, comment) => {
try{
const session = await glpi.initSession(); const session = await glpi.initSession();
let token = session.data.session_token; let token = session.data.session_token;
const response = await axios(conf.glpiConfig.apiurl + '/ITILFollowup/', { const response = await axios(conf.glpiConfig.apiurl + '/ITILFollowup/', {
@@ -111,11 +138,15 @@ exports.addComment = async(ticketId, comment) => {
content: comment content: comment
} }
}) })
}).catch((err) => console.log(err)); });
return response.data; return response.data;
}catch(e){
fs.appendFileSync(__dirname + "/../logs/logs.json", JSON.stringify(e, null, 3));
}
} }
exports.removeComment = async(ticketId, commentId) => { exports.removeComment = async(ticketId, commentId) => {
try{
const session = await glpi.initSession(); const session = await glpi.initSession();
let token = session.data.session_token; let token = session.data.session_token;
const response = await axios(conf.glpiConfig.apiurl + '/ITILFollowup/' + commentId, { const response = await axios(conf.glpiConfig.apiurl + '/ITILFollowup/' + commentId, {
@@ -132,11 +163,15 @@ exports.removeComment = async(ticketId, commentId) => {
itemtype: "Ticket" itemtype: "Ticket"
} }
}) })
}).catch((err) => console.log(err)); });
return response.data; return response.data;
}catch(e){
fs.appendFileSync(__dirname + "/../logs/logs.json", JSON.stringify(e, null, 3));
}
} }
exports.getUsers = async(ticketId) => { exports.getUsers = async(ticketId) => {
try{
const session = await glpi.initSession(); const session = await glpi.initSession();
let token = session.data.session_token; let token = session.data.session_token;
const response = await axios(`${conf.glpiConfig.apiurl}/Ticket/${ticketId}/Ticket_User`, { const response = await axios(`${conf.glpiConfig.apiurl}/Ticket/${ticketId}/Ticket_User`, {
@@ -147,6 +182,9 @@ exports.getUsers = async(ticketId) => {
'Authorization': conf.glpiConfig.user_token, 'Authorization': conf.glpiConfig.user_token,
'Session-Token': token 'Session-Token': token
} }
}).catch((err) => console.log(err)); });
return response.data; return response.data;
}catch(e){
fs.appendFileSync(__dirname + "/../logs/logs.json", JSON.stringify(e, null, 3));
}
} }

View File

@@ -1,11 +1,13 @@
const he = require('he'); const he = require('he');
const html = require('html-to-text'); const html = require('html-to-text');
const fs = require('fs'); const fs = require('fs');
const cns = require('./const.js');
const conf = JSON.parse(fs.readFileSync(__dirname + "/../data/conf.json")); const dir = __dirname;
const conf = JSON.parse(fs.readFileSync(dir + "/../data/conf.json"));
const glpiUrl = conf.glpiConfig.apiurl.replace("apirest.php", ""); const glpiUrl = conf.glpiConfig.apiurl.replace("apirest.php", "");
exports.htmlToText = async(text) => { exports.htmlToText = async (text) => {
let temp = html.convert(he.decode(text), {preserveNewlines: true}); let temp = html.convert(he.decode(text), {preserveNewlines: true});
textArray = temp.split('\n'); textArray = temp.split('\n');
for(let i in textArray){ for(let i in textArray){
@@ -16,48 +18,96 @@ exports.htmlToText = async(text) => {
} }
let messageText = ''; let messageText = '';
for(let k in textArray){ for(let k in textArray){
if(textArray[k][0] != '>' && textArray[k].trim() && textArray[k].indexOf('cellpadding="0"') == -1){ if(textArray[k][0] != '>' && textArray[k].trim() && textArray[k].indexOf('ts@krtech.ru писал') == -1 && textArray[k].indexOf('cellpadding="0"') == -1){
messageText += textArray[k].trim().replace(/[<>]/g, '') + ' '; messageText += textArray[k].trim().replace(/[<>]/g, '') + ' ';
} }
} }
return messageText.replace(/\[.*?\]/g, ''); return messageText.replace(/\[.*?\]/g, '');
} }
exports.parseMessageText = async(message, addSilentInfo) => { exports.parseMessageText = async (message, messageData, ticketId) => {
let color = '🟢'; let color = await this.getTicketColor(messageData.data[ticketId].status);
if(message.hasOwnProperty('status')){
switch(message.status){
case 1: color = '🟢'; break;
case 2: color = '🔵'; break;
case 4: color = '🟠'; break;
case 6: color = '⚫'; break;
default: color = '⚫';
}
}
let ticketId = message.text.split('\n')[0].split('№')[1];
if(!addSilentInfo){
addSilentInfo = '';
}else addSilentInfo = '/' + addSilentInfo;
let silentInfo = `<a href="${message.entities[0].url}${addSilentInfo}">&#8203</a>`;
text = message.text.split('\n'); text = message.text.split('\n');
messageText = `${silentInfo}${color} <b>ЗАЯВКА <a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">№${ticketId}</a></b>\n\n`; messageText = `${color} <b>ЗАЯВКА <a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">№${ticketId}</a></b>\n\n`;
for(let i = 2; i < text.length; i++){ for(let i = 2; i < text.length; i++){
if(text[i].indexOf(':') >= 0) messageText += `<b>${text[i].replace(':', ':</b>')}\n`; if(text[i].indexOf(':') >= 0) messageText += `<b>${text[i].replace(':', ':</b>')}\n`;
} }
for(let i in message.entities){
if(message.entities[i].type == 'text_link' && message.entities[i].url.indexOf(glpiUrl + 'front/document.send.php?docid=') > 0){
messageText = messageText.substring(0, messageText.length - 1).replace('screen', '');
messageText += `<a href="${message.entities[i].url}">screen</a>`;
}
}
if(text[text.length - 1].indexOf("Читать дальше") >= 0){ if(text[text.length - 1].indexOf("Читать дальше") >= 0){
messageText = `${messageText.replace('Читать дальше', '')}\n<b><a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">Читать дальше</a></b>`; messageText = `${messageText.replace('Читать дальше', '')}\n<b><a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">Читать дальше</a></b>`;
} }
return messageText; return messageText;
} }
exports.sleep = async(ms) => { exports.createThread = async (bot, messageData, ticketId, title) => {
let td = messageData.data[ticketId];
let thread = await bot.telegram.createForumTopic(conf.supportChatId, title, {
icon_custom_emoji_id: '5357315181649076022'
});
let status = td.status;
messageData.data[ticketId].threadId = thread.message_thread_id;
let inKeyboard = await this.getKeyboardFromStatus(status);
await this.editMessageMarkup(bot, td.messageId, inKeyboard);
let msg = await bot.telegram.copyMessage(conf.supportChatId, conf.supportChatId, td.messageId, {
parse_mode: 'HTML',
disable_notification: true,
message_thread_id: thread.message_thread_id,
reply_markup: { inline_keyboard: inKeyboard }
});
await bot.telegram.pinChatMessage(conf.supportChatId, msg.message_id, { disable_notification: true });
messageData.data[ticketId].pinMessageId = msg.message_id;
fs.writeFileSync(dir + "/../data/messageData.json", JSON.stringify(messageData, null, 3));
}
exports.closeThread = async (bot, messageData, ticketId) => {
await bot.telegram.deleteForumTopic(conf.supportChatId, messageData.data[ticketId].threadId);
delete messageData.data[ticketId].threadId;
delete messageData.data[ticketId].pinMessageId;
let jsonData = JSON.stringify(messageData, null, 3);
fs.writeFileSync(dir + "/../data/messageData.json", jsonData);
}
exports.getTicketColor = async (status) => {
switch(status){
case 1: color = '🟢'; break;
case 2: color = '🔵'; break;
case 3: color = '🔵'; break;
case 4: color = '🟠'; break;
case 6: color = '⚫'; break;
default: color = '⚫';
}
return color;
}
exports.getKeyboardFromStatus = async (status) => {
let keyboard = cns.inlineKeyboards.open;
if(status == 5 || status == 6){
keyboard = cns.inlineKeyboards.close;
}
return keyboard;
}
exports.editMessageText = async (bot, messageId, messageText, keyboard) => {
try{
await bot.telegram.editMessageText(conf.supportChatId, messageId, undefined, messageText, {
parse_mode: 'HTML',
reply_markup: {inline_keyboard: keyboard}
});
}catch(e){
fs.appendFileSync(dir + "/../logs/logs.json", JSON.stringify(e, null, 3));
}
}
exports.editMessageMarkup = async (bot, messageId, keyboard) => {
try{
await bot.telegram.editMessageReplyMarkup(conf.supportChatId, messageId, undefined, {inline_keyboard: keyboard});
}catch(e){
fs.appendFileSync(dir + "/../logs/logs.json", JSON.stringify(e, null, 3));
}
}
exports.sleep = async (ms) => {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(resolve, ms); setTimeout(resolve, ms);
}); });
} }

0
logs/logs.json Normal file
View File

1
node_modules/.bin/he generated vendored
View File

@@ -1 +0,0 @@
../he/bin/he

148
node_modules/.bin/he generated vendored Executable file
View File

@@ -0,0 +1,148 @@
#!/usr/bin/env node
(function() {
var fs = require('fs');
var he = require('../he.js');
var strings = process.argv.splice(2);
var stdin = process.stdin;
var data;
var timeout;
var action;
var options = {};
var log = console.log;
var main = function() {
var option = strings[0];
var count = 0;
if (/^(?:-h|--help|undefined)$/.test(option)) {
log(
'he v%s - https://mths.be/he',
he.version
);
log([
'\nUsage:\n',
'\the [--escape] string',
'\the [--encode] [--use-named-refs] [--everything] [--allow-unsafe] [--decimal] string',
'\the [--decode] [--attribute] [--strict] string',
'\the [-v | --version]',
'\the [-h | --help]',
'\nExamples:\n',
'\the --escape \\<img\\ src\\=\\\'x\\\'\\ onerror\\=\\"prompt\\(1\\)\\"\\>',
'\techo \'&copy; &#x1D306;\' | he --decode'
].join('\n'));
return process.exit(option ? 0 : 1);
}
if (/^(?:-v|--version)$/.test(option)) {
log('v%s', he.version);
return process.exit(0);
}
strings.forEach(function(string) {
// Process options
if (string == '--escape') {
action = 'escape';
return;
}
if (string == '--encode') {
action = 'encode';
return;
}
if (string == '--use-named-refs') {
action = 'encode';
options.useNamedReferences = true;
return;
}
if (string == '--everything') {
action = 'encode';
options.encodeEverything = true;
return;
}
if (string == '--allow-unsafe') {
action = 'encode';
options.allowUnsafeSymbols = true;
return;
}
if (string == '--decimal') {
action = 'encode';
options.decimal = true;
return;
}
if (string == '--decode') {
action = 'decode';
return;
}
if (string == '--attribute') {
action = 'decode';
options.isAttributeValue = true;
return;
}
if (string == '--strict') {
action = 'decode';
options.strict = true;
return;
}
// Process string(s)
var result;
if (!action) {
log('Error: he requires at least one option and a string argument.');
log('Try `he --help` for more information.');
return process.exit(1);
}
try {
result = he[action](string, options);
log(result);
count++;
} catch(error) {
log(error.message + '\n');
log('Error: failed to %s.', action);
log('If you think this is a bug in he, please report it:');
log('https://github.com/mathiasbynens/he/issues/new');
log(
'\nStack trace using he@%s:\n',
he.version
);
log(error.stack);
return process.exit(1);
}
});
if (!count) {
log('Error: he requires a string argument.');
log('Try `he --help` for more information.');
return process.exit(1);
}
// Return with exit status 0 outside of the `forEach` loop, in case
// multiple strings were passed in.
return process.exit(0);
};
if (stdin.isTTY) {
// handle shell arguments
main();
} else {
// Either the script is called from within a non-TTY context, or `stdin`
// content is being piped in.
if (!process.stdout.isTTY) {
// The script was called from a non-TTY context. This is a rather uncommon
// use case we dont actively support. However, we dont want the script
// to wait forever in such cases, so…
timeout = setTimeout(function() {
// …if no piped data arrived after a whole minute, handle shell
// arguments instead.
main();
}, 60000);
}
data = '';
stdin.on('data', function(chunk) {
clearTimeout(timeout);
data += chunk;
});
stdin.on('end', function() {
strings.push(data.trim());
main();
});
stdin.resume();
}
}());

1
node_modules/.bin/nodemon generated vendored
View File

@@ -1 +0,0 @@
../nodemon/bin/nodemon.js

16
node_modules/.bin/nodemon generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env node
const cli = require('../lib/cli');
const nodemon = require('../lib/');
const options = cli.parse(process.argv);
nodemon(options);
const fs = require('fs');
// checks for available update and returns an instance
const pkg = JSON.parse(fs.readFileSync(__dirname + '/../package.json'));
if (pkg.version.indexOf('0.0.0') !== 0 && options.noUpdateNotifier !== true) {
require('simple-update-notifier')({ pkg });
}

1
node_modules/.bin/nodetouch generated vendored
View File

@@ -1 +0,0 @@
../touch/bin/nodetouch.js

112
node_modules/.bin/nodetouch generated vendored Executable file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/env node
const touch = require("../index.js")
const usage = code => {
console[code ? 'error' : 'log'](
'usage:\n' +
'touch [-acfm] [-r file] [-t [[CC]YY]MMDDhhmm[.SS]] file ...'
)
process.exit(code)
}
const singleFlags = {
a: 'atime',
m: 'mtime',
c: 'nocreate',
f: 'force'
}
const singleOpts = {
r: 'ref',
t: 'time'
}
const files = []
const args = process.argv.slice(2)
const options = {}
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (!arg.match(/^-/)) {
files.push(arg)
continue
}
// expand shorthands
if (arg.charAt(1) !== '-') {
const expand = []
for (let f = 1; f < arg.length; f++) {
const fc = arg.charAt(f)
const sf = singleFlags[fc]
const so = singleOpts[fc]
if (sf)
expand.push('--' + sf)
else if (so) {
const soslice = arg.slice(f + 1)
const soval = soslice.charAt(0) === '=' ? soslice : '=' + soslice
expand.push('--' + so + soval)
f = arg.length
} else if (arg !== '-' + fc)
expand.push('-' + fc)
}
if (expand.length) {
args.splice.apply(args, [i, 1].concat(expand))
i--
continue
}
}
const argsplit = arg.split('=')
const key = argsplit.shift().replace(/^\-\-/, '')
const val = argsplit.length ? argsplit.join('=') : null
switch (key) {
case 'time':
const timestr = val || args[++i]
// [-t [[CC]YY]MMDDhhmm[.SS]]
const parsedtime = timestr.match(
/^(([0-9]{2})?([0-9]{2}))?([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})(\.([0-9]{2}))?$/
)
if (!parsedtime) {
console.error('touch: out of range or illegal ' +
'time specification: ' +
'[[CC]YY]MMDDhhmm[.SS]')
process.exit(1)
} else {
const y = +parsedtime[1]
const year = parsedtime[2] ? y
: y <= 68 ? 2000 + y
: 1900 + y
const MM = +parsedtime[4] - 1
const dd = +parsedtime[5]
const hh = +parsedtime[6]
const mm = +parsedtime[7]
const ss = +parsedtime[8]
options.time = new Date(Date.UTC(year, MM, dd, hh, mm, ss))
}
continue
case 'ref':
options.ref = val || args[++i]
continue
case 'mtime':
case 'nocreate':
case 'atime':
case 'force':
options[key] = true
continue
default:
console.error('touch: illegal option -- ' + arg)
usage(1)
}
}
if (!files.length)
usage()
process.exitCode = 0
Promise.all(files.map(f => touch(f, options)))
.catch(er => process.exitCode = 1)

1
node_modules/.bin/nopt generated vendored
View File

@@ -1 +0,0 @@
../nopt/bin/nopt.js

44
node_modules/.bin/nopt generated vendored Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env node
var nopt = require("../lib/nopt")
, types = { num: Number
, bool: Boolean
, help: Boolean
, list: Array
, "num-list": [Number, Array]
, "str-list": [String, Array]
, "bool-list": [Boolean, Array]
, str: String }
, shorthands = { s: [ "--str", "astring" ]
, b: [ "--bool" ]
, nb: [ "--no-bool" ]
, tft: [ "--bool-list", "--no-bool-list", "--bool-list", "true" ]
, "?": ["--help"]
, h: ["--help"]
, H: ["--help"]
, n: [ "--num", "125" ] }
, parsed = nopt( types
, shorthands
, process.argv
, 2 )
console.log("parsed", parsed)
if (parsed.help) {
console.log("")
console.log("nopt cli tester")
console.log("")
console.log("types")
console.log(Object.keys(types).map(function M (t) {
var type = types[t]
if (Array.isArray(type)) {
return [t, type.map(function (type) { return type.name })]
}
return [t, type && type.name]
}).reduce(function (s, i) {
s[i[0]] = i[1]
return s
}, {}))
console.log("")
console.log("shorthands")
console.log(shorthands)
}

1
node_modules/.bin/semver generated vendored
View File

@@ -1 +0,0 @@
../semver/bin/semver.js

197
node_modules/.bin/semver generated vendored Executable file
View File

@@ -0,0 +1,197 @@
#!/usr/bin/env node
// Standalone semver comparison program.
// Exits successfully and prints matching version(s) if
// any supplied version is valid and passes all tests.
const argv = process.argv.slice(2)
let versions = []
const range = []
let inc = null
const version = require('../package.json').version
let loose = false
let includePrerelease = false
let coerce = false
let rtl = false
let identifier
let identifierBase
const semver = require('../')
const parseOptions = require('../internal/parse-options')
let reverse = false
let options = {}
const main = () => {
if (!argv.length) {
return help()
}
while (argv.length) {
let a = argv.shift()
const indexOfEqualSign = a.indexOf('=')
if (indexOfEqualSign !== -1) {
const value = a.slice(indexOfEqualSign + 1)
a = a.slice(0, indexOfEqualSign)
argv.unshift(value)
}
switch (a) {
case '-rv': case '-rev': case '--rev': case '--reverse':
reverse = true
break
case '-l': case '--loose':
loose = true
break
case '-p': case '--include-prerelease':
includePrerelease = true
break
case '-v': case '--version':
versions.push(argv.shift())
break
case '-i': case '--inc': case '--increment':
switch (argv[0]) {
case 'major': case 'minor': case 'patch': case 'prerelease':
case 'premajor': case 'preminor': case 'prepatch':
inc = argv.shift()
break
default:
inc = 'patch'
break
}
break
case '--preid':
identifier = argv.shift()
break
case '-r': case '--range':
range.push(argv.shift())
break
case '-n':
identifierBase = argv.shift()
if (identifierBase === 'false') {
identifierBase = false
}
break
case '-c': case '--coerce':
coerce = true
break
case '--rtl':
rtl = true
break
case '--ltr':
rtl = false
break
case '-h': case '--help': case '-?':
return help()
default:
versions.push(a)
break
}
}
options = parseOptions({ loose, includePrerelease, rtl })
versions = versions.map((v) => {
return coerce ? (semver.coerce(v, options) || { version: v }).version : v
}).filter((v) => {
return semver.valid(v)
})
if (!versions.length) {
return fail()
}
if (inc && (versions.length !== 1 || range.length)) {
return failInc()
}
for (let i = 0, l = range.length; i < l; i++) {
versions = versions.filter((v) => {
return semver.satisfies(v, range[i], options)
})
if (!versions.length) {
return fail()
}
}
return success(versions)
}
const failInc = () => {
console.error('--inc can only be used on a single version with no range')
fail()
}
const fail = () => process.exit(1)
const success = () => {
const compare = reverse ? 'rcompare' : 'compare'
versions.sort((a, b) => {
return semver[compare](a, b, options)
}).map((v) => {
return semver.clean(v, options)
}).map((v) => {
return inc ? semver.inc(v, inc, options, identifier, identifierBase) : v
}).forEach((v, i, _) => {
console.log(v)
})
}
const help = () => console.log(
`SemVer ${version}
A JavaScript implementation of the https://semver.org/ specification
Copyright Isaac Z. Schlueter
Usage: semver [options] <version> [<version> [...]]
Prints valid versions sorted by SemVer precedence
Options:
-r --range <range>
Print versions that match the specified range.
-i --increment [<level>]
Increment a version by the specified level. Level can
be one of: major, minor, patch, premajor, preminor,
prepatch, or prerelease. Default level is 'patch'.
Only one version may be specified.
--preid <identifier>
Identifier to be used to prefix premajor, preminor,
prepatch or prerelease version increments.
-l --loose
Interpret versions and ranges loosely
-p --include-prerelease
Always include prerelease versions in range matching
-c --coerce
Coerce a string into SemVer if possible
(does not imply --loose)
--rtl
Coerce version strings right to left
--ltr
Coerce version strings left to right (default)
-n <base>
Base number to be used for the prerelease identifier.
Can be either 0 or 1, or false to omit the number altogether.
Defaults to 0.
Program exits successfully if any valid version satisfies
all supplied ranges, and prints all satisfying versions.
If no satisfying versions are found, then exits failure.
Versions are printed in ascending order, so supplying
multiple versions to the utility will just sort them.`)
main()

1
node_modules/.bin/sshpk-conv generated vendored
View File

@@ -1 +0,0 @@
../sshpk/bin/sshpk-conv

243
node_modules/.bin/sshpk-conv generated vendored Executable file
View File

@@ -0,0 +1,243 @@
#!/usr/bin/env node
// -*- mode: js -*-
// vim: set filetype=javascript :
// Copyright 2018 Joyent, Inc. All rights reserved.
var dashdash = require('dashdash');
var sshpk = require('../lib/index');
var fs = require('fs');
var path = require('path');
var tty = require('tty');
var readline = require('readline');
var getPassword = require('getpass').getPass;
var options = [
{
names: ['outformat', 't'],
type: 'string',
help: 'Output format'
},
{
names: ['informat', 'T'],
type: 'string',
help: 'Input format'
},
{
names: ['file', 'f'],
type: 'string',
help: 'Input file name (default stdin)'
},
{
names: ['out', 'o'],
type: 'string',
help: 'Output file name (default stdout)'
},
{
names: ['private', 'p'],
type: 'bool',
help: 'Produce a private key as output'
},
{
names: ['derive', 'd'],
type: 'string',
help: 'Output a new key derived from this one, with given algo'
},
{
names: ['identify', 'i'],
type: 'bool',
help: 'Print key metadata instead of converting'
},
{
names: ['fingerprint', 'F'],
type: 'bool',
help: 'Output key fingerprint'
},
{
names: ['hash', 'H'],
type: 'string',
help: 'Hash function to use for key fingeprint with -F'
},
{
names: ['spki', 's'],
type: 'bool',
help: 'With -F, generates an SPKI fingerprint instead of SSH'
},
{
names: ['comment', 'c'],
type: 'string',
help: 'Set key comment, if output format supports'
},
{
names: ['help', 'h'],
type: 'bool',
help: 'Shows this help text'
}
];
if (require.main === module) {
var parser = dashdash.createParser({
options: options
});
try {
var opts = parser.parse(process.argv);
} catch (e) {
console.error('sshpk-conv: error: %s', e.message);
process.exit(1);
}
if (opts.help || opts._args.length > 1) {
var help = parser.help({}).trimRight();
console.error('sshpk-conv: converts between SSH key formats\n');
console.error(help);
console.error('\navailable key formats:');
console.error(' - pem, pkcs1 eg id_rsa');
console.error(' - ssh eg id_rsa.pub');
console.error(' - pkcs8 format you want for openssl');
console.error(' - openssh like output of ssh-keygen -o');
console.error(' - rfc4253 raw OpenSSH wire format');
console.error(' - dnssec dnssec-keygen format');
console.error(' - putty PuTTY ppk format');
console.error('\navailable fingerprint formats:');
console.error(' - hex colon-separated hex for SSH');
console.error(' straight hex for SPKI');
console.error(' - base64 SHA256:* format from OpenSSH');
process.exit(1);
}
/*
* Key derivation can only be done on private keys, so use of the -d
* option necessarily implies -p.
*/
if (opts.derive)
opts.private = true;
var inFile = process.stdin;
var inFileName = 'stdin';
var inFilePath;
if (opts.file) {
inFilePath = opts.file;
} else if (opts._args.length === 1) {
inFilePath = opts._args[0];
}
if (inFilePath)
inFileName = path.basename(inFilePath);
try {
if (inFilePath) {
fs.accessSync(inFilePath, fs.R_OK);
inFile = fs.createReadStream(inFilePath);
}
} catch (e) {
ifError(e, 'error opening input file');
}
var outFile = process.stdout;
try {
if (opts.out && !opts.identify) {
fs.accessSync(path.dirname(opts.out), fs.W_OK);
outFile = fs.createWriteStream(opts.out);
}
} catch (e) {
ifError(e, 'error opening output file');
}
var bufs = [];
inFile.on('readable', function () {
var data;
while ((data = inFile.read()))
bufs.push(data);
});
var parseOpts = {};
parseOpts.filename = inFileName;
inFile.on('end', function processKey() {
var buf = Buffer.concat(bufs);
var fmt = 'auto';
if (opts.informat)
fmt = opts.informat;
var f = sshpk.parseKey;
if (opts.private)
f = sshpk.parsePrivateKey;
try {
var key = f(buf, fmt, parseOpts);
} catch (e) {
if (e.name === 'KeyEncryptedError') {
getPassword(function (err, pw) {
if (err)
ifError(err);
parseOpts.passphrase = pw;
processKey();
});
return;
}
ifError(e);
}
if (opts.derive)
key = key.derive(opts.derive);
if (opts.comment)
key.comment = opts.comment;
if (opts.identify) {
var kind = 'public';
if (sshpk.PrivateKey.isPrivateKey(key))
kind = 'private';
console.log('%s: a %d bit %s %s key', inFileName,
key.size, key.type.toUpperCase(), kind);
if (key.type === 'ecdsa')
console.log('ECDSA curve: %s', key.curve);
if (key.comment)
console.log('Comment: %s', key.comment);
console.log('SHA256 fingerprint: ' +
key.fingerprint('sha256').toString());
console.log('MD5 fingerprint: ' +
key.fingerprint('md5').toString());
console.log('SPKI-SHA256 fingerprint: ' +
key.fingerprint('sha256', 'spki').toString());
process.exit(0);
return;
}
if (opts.fingerprint) {
var hash = opts.hash;
var type = opts.spki ? 'spki' : 'ssh';
var format = opts.outformat;
var fp = key.fingerprint(hash, type).toString(format);
outFile.write(fp);
outFile.write('\n');
outFile.once('drain', function () {
process.exit(0);
});
return;
}
fmt = undefined;
if (opts.outformat)
fmt = opts.outformat;
outFile.write(key.toBuffer(fmt));
if (fmt === 'ssh' ||
(!opts.private && fmt === undefined))
outFile.write('\n');
outFile.once('drain', function () {
process.exit(0);
});
});
}
function ifError(e, txt) {
if (txt)
txt = txt + ': ';
else
txt = '';
console.error('sshpk-conv: ' + txt + e.name + ': ' + e.message);
if (process.env['DEBUG'] || process.env['V']) {
console.error(e.stack);
if (e.innerErr)
console.error(e.innerErr.stack);
}
process.exit(1);
}

1
node_modules/.bin/sshpk-sign generated vendored
View File

@@ -1 +0,0 @@
../sshpk/bin/sshpk-sign

191
node_modules/.bin/sshpk-sign generated vendored Executable file
View File

@@ -0,0 +1,191 @@
#!/usr/bin/env node
// -*- mode: js -*-
// vim: set filetype=javascript :
// Copyright 2015 Joyent, Inc. All rights reserved.
var dashdash = require('dashdash');
var sshpk = require('../lib/index');
var fs = require('fs');
var path = require('path');
var getPassword = require('getpass').getPass;
var options = [
{
names: ['hash', 'H'],
type: 'string',
help: 'Hash algorithm (sha1, sha256, sha384, sha512)'
},
{
names: ['verbose', 'v'],
type: 'bool',
help: 'Display verbose info about key and hash used'
},
{
names: ['identity', 'i'],
type: 'string',
help: 'Path to key to use'
},
{
names: ['file', 'f'],
type: 'string',
help: 'Input filename'
},
{
names: ['out', 'o'],
type: 'string',
help: 'Output filename'
},
{
names: ['format', 't'],
type: 'string',
help: 'Signature format (asn1, ssh, raw)'
},
{
names: ['binary', 'b'],
type: 'bool',
help: 'Output raw binary instead of base64'
},
{
names: ['help', 'h'],
type: 'bool',
help: 'Shows this help text'
}
];
var parseOpts = {};
if (require.main === module) {
var parser = dashdash.createParser({
options: options
});
try {
var opts = parser.parse(process.argv);
} catch (e) {
console.error('sshpk-sign: error: %s', e.message);
process.exit(1);
}
if (opts.help || opts._args.length > 1) {
var help = parser.help({}).trimRight();
console.error('sshpk-sign: sign data using an SSH key\n');
console.error(help);
process.exit(1);
}
if (!opts.identity) {
var help = parser.help({}).trimRight();
console.error('sshpk-sign: the -i or --identity option ' +
'is required\n');
console.error(help);
process.exit(1);
}
var keyData = fs.readFileSync(opts.identity);
parseOpts.filename = opts.identity;
run();
}
function run() {
var key;
try {
key = sshpk.parsePrivateKey(keyData, 'auto', parseOpts);
} catch (e) {
if (e.name === 'KeyEncryptedError') {
getPassword(function (err, pw) {
parseOpts.passphrase = pw;
run();
});
return;
}
console.error('sshpk-sign: error loading private key "' +
opts.identity + '": ' + e.name + ': ' + e.message);
process.exit(1);
}
var hash = opts.hash || key.defaultHashAlgorithm();
var signer;
try {
signer = key.createSign(hash);
} catch (e) {
console.error('sshpk-sign: error creating signer: ' +
e.name + ': ' + e.message);
process.exit(1);
}
if (opts.verbose) {
console.error('sshpk-sign: using %s-%s with a %d bit key',
key.type, hash, key.size);
}
var inFile = process.stdin;
var inFileName = 'stdin';
var inFilePath;
if (opts.file) {
inFilePath = opts.file;
} else if (opts._args.length === 1) {
inFilePath = opts._args[0];
}
if (inFilePath)
inFileName = path.basename(inFilePath);
try {
if (inFilePath) {
fs.accessSync(inFilePath, fs.R_OK);
inFile = fs.createReadStream(inFilePath);
}
} catch (e) {
console.error('sshpk-sign: error opening input file' +
': ' + e.name + ': ' + e.message);
process.exit(1);
}
var outFile = process.stdout;
try {
if (opts.out && !opts.identify) {
fs.accessSync(path.dirname(opts.out), fs.W_OK);
outFile = fs.createWriteStream(opts.out);
}
} catch (e) {
console.error('sshpk-sign: error opening output file' +
': ' + e.name + ': ' + e.message);
process.exit(1);
}
inFile.pipe(signer);
inFile.on('end', function () {
var sig;
try {
sig = signer.sign();
} catch (e) {
console.error('sshpk-sign: error signing data: ' +
e.name + ': ' + e.message);
process.exit(1);
}
var fmt = opts.format || 'asn1';
var output;
try {
output = sig.toBuffer(fmt);
if (!opts.binary)
output = output.toString('base64');
} catch (e) {
console.error('sshpk-sign: error converting signature' +
' to ' + fmt + ' format: ' + e.name + ': ' +
e.message);
process.exit(1);
}
outFile.write(output);
if (!opts.binary)
outFile.write('\n');
outFile.once('drain', function () {
process.exit(0);
});
});
}

1
node_modules/.bin/sshpk-verify generated vendored
View File

@@ -1 +0,0 @@
../sshpk/bin/sshpk-verify

167
node_modules/.bin/sshpk-verify generated vendored Executable file
View File

@@ -0,0 +1,167 @@
#!/usr/bin/env node
// -*- mode: js -*-
// vim: set filetype=javascript :
// Copyright 2015 Joyent, Inc. All rights reserved.
var dashdash = require('dashdash');
var sshpk = require('../lib/index');
var fs = require('fs');
var path = require('path');
var Buffer = require('safer-buffer').Buffer;
var options = [
{
names: ['hash', 'H'],
type: 'string',
help: 'Hash algorithm (sha1, sha256, sha384, sha512)'
},
{
names: ['verbose', 'v'],
type: 'bool',
help: 'Display verbose info about key and hash used'
},
{
names: ['identity', 'i'],
type: 'string',
help: 'Path to (public) key to use'
},
{
names: ['file', 'f'],
type: 'string',
help: 'Input filename'
},
{
names: ['format', 't'],
type: 'string',
help: 'Signature format (asn1, ssh, raw)'
},
{
names: ['signature', 's'],
type: 'string',
help: 'base64-encoded signature data'
},
{
names: ['help', 'h'],
type: 'bool',
help: 'Shows this help text'
}
];
if (require.main === module) {
var parser = dashdash.createParser({
options: options
});
try {
var opts = parser.parse(process.argv);
} catch (e) {
console.error('sshpk-verify: error: %s', e.message);
process.exit(3);
}
if (opts.help || opts._args.length > 1) {
var help = parser.help({}).trimRight();
console.error('sshpk-verify: sign data using an SSH key\n');
console.error(help);
process.exit(3);
}
if (!opts.identity) {
var help = parser.help({}).trimRight();
console.error('sshpk-verify: the -i or --identity option ' +
'is required\n');
console.error(help);
process.exit(3);
}
if (!opts.signature) {
var help = parser.help({}).trimRight();
console.error('sshpk-verify: the -s or --signature option ' +
'is required\n');
console.error(help);
process.exit(3);
}
var keyData = fs.readFileSync(opts.identity);
var key;
try {
key = sshpk.parseKey(keyData);
} catch (e) {
console.error('sshpk-verify: error loading key "' +
opts.identity + '": ' + e.name + ': ' + e.message);
process.exit(2);
}
var fmt = opts.format || 'asn1';
var sigData = Buffer.from(opts.signature, 'base64');
var sig;
try {
sig = sshpk.parseSignature(sigData, key.type, fmt);
} catch (e) {
console.error('sshpk-verify: error parsing signature: ' +
e.name + ': ' + e.message);
process.exit(2);
}
var hash = opts.hash || key.defaultHashAlgorithm();
var verifier;
try {
verifier = key.createVerify(hash);
} catch (e) {
console.error('sshpk-verify: error creating verifier: ' +
e.name + ': ' + e.message);
process.exit(2);
}
if (opts.verbose) {
console.error('sshpk-verify: using %s-%s with a %d bit key',
key.type, hash, key.size);
}
var inFile = process.stdin;
var inFileName = 'stdin';
var inFilePath;
if (opts.file) {
inFilePath = opts.file;
} else if (opts._args.length === 1) {
inFilePath = opts._args[0];
}
if (inFilePath)
inFileName = path.basename(inFilePath);
try {
if (inFilePath) {
fs.accessSync(inFilePath, fs.R_OK);
inFile = fs.createReadStream(inFilePath);
}
} catch (e) {
console.error('sshpk-verify: error opening input file' +
': ' + e.name + ': ' + e.message);
process.exit(2);
}
inFile.pipe(verifier);
inFile.on('end', function () {
var ret;
try {
ret = verifier.verify(sig);
} catch (e) {
console.error('sshpk-verify: error verifying data: ' +
e.name + ': ' + e.message);
process.exit(1);
}
if (ret) {
console.error('OK');
process.exit(0);
}
console.error('NOT OK');
process.exit(1);
});
}

1
node_modules/.bin/telegraf generated vendored
View File

@@ -1 +0,0 @@
../telegraf/lib/cli.mjs

105
node_modules/.bin/telegraf generated vendored Executable file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env node
import d from 'debug';
import parse from 'mri';
import path from 'path';
import { Telegraf } from './index.js';
const debug = d('telegraf:cli');
const helpMsg = `Usage: telegraf [opts] <bot-file>
-t Bot token [$BOT_TOKEN]
-d Webhook domain [$BOT_DOMAIN]
-H Webhook host [0.0.0.0]
-p Webhook port [$PORT or 3000]
-l Enable logs
-h Show this help message
-m Bot API method to run directly
-D Data to pass to the Bot API method`;
const help = () => console.log(helpMsg);
/**
* Runs the cli program and returns exit code
*/
export async function main(argv, env = {}) {
const args = parse(argv, {
alias: {
// string params, all optional
t: 'token',
d: 'domain',
m: 'method',
D: 'data',
// defaults exist
H: 'host',
p: 'port',
// boolean params
l: 'logs',
h: 'help',
},
boolean: ['h', 'l'],
default: {
H: '0.0.0.0',
p: env.PORT || '3000',
},
});
if (args.help) {
help();
return 0;
}
const token = args.token || env.BOT_TOKEN;
const domain = args.domain || env.BOT_DOMAIN;
if (!token) {
console.error('Please supply Bot Token');
help();
return 1;
}
const bot = new Telegraf(token);
if (args.method) {
const method = args.method;
console.log(await bot.telegram.callApi(method, JSON.parse(args.data || '{}')));
return 0;
}
let [, , file] = args._;
if (!file) {
try {
const packageJson = (await import(path.resolve(process.cwd(), 'package.json')));
file = packageJson.main || 'index.js';
// eslint-disable-next-line no-empty
}
catch (err) { }
}
if (!file) {
console.error('Please supply a bot handler file.\n');
help();
return 2;
}
if (file[0] !== '/')
file = path.resolve(process.cwd(), file);
try {
if (args.logs)
d.enable('telegraf:*');
const mod = await import(file);
const botHandler = mod.botHandler || mod.default;
const httpHandler = mod.httpHandler;
const tlsOptions = mod.tlsOptions;
const config = {};
if (domain) {
config.webhook = {
domain,
host: args.host,
port: Number(args.port),
tlsOptions,
cb: httpHandler,
};
}
bot.use(botHandler);
debug(`Starting module ${file}`);
await bot.launch(config);
}
catch (err) {
console.error(`Error launching bot from ${file}`, err === null || err === void 0 ? void 0 : err.stack);
return 3;
}
// Enable graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'));
process.once('SIGTERM', () => bot.stop('SIGTERM'));
return 0;
}
process.exitCode = await main(process.argv, process.env);

1
node_modules/.bin/uuid generated vendored
View File

@@ -1 +0,0 @@
../uuid/bin/uuid

65
node_modules/.bin/uuid generated vendored Executable file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env node
var assert = require('assert');
function usage() {
console.log('Usage:');
console.log(' uuid');
console.log(' uuid v1');
console.log(' uuid v3 <name> <namespace uuid>');
console.log(' uuid v4');
console.log(' uuid v5 <name> <namespace uuid>');
console.log(' uuid --help');
console.log('\nNote: <namespace uuid> may be "URL" or "DNS" to use the corresponding UUIDs defined by RFC4122');
}
var args = process.argv.slice(2);
if (args.indexOf('--help') >= 0) {
usage();
process.exit(0);
}
var version = args.shift() || 'v4';
switch (version) {
case 'v1':
var uuidV1 = require('../v1');
console.log(uuidV1());
break;
case 'v3':
var uuidV3 = require('../v3');
var name = args.shift();
var namespace = args.shift();
assert(name != null, 'v3 name not specified');
assert(namespace != null, 'v3 namespace not specified');
if (namespace == 'URL') namespace = uuidV3.URL;
if (namespace == 'DNS') namespace = uuidV3.DNS;
console.log(uuidV3(name, namespace));
break;
case 'v4':
var uuidV4 = require('../v4');
console.log(uuidV4());
break;
case 'v5':
var uuidV5 = require('../v5');
var name = args.shift();
var namespace = args.shift();
assert(name != null, 'v5 name not specified');
assert(namespace != null, 'v5 namespace not specified');
if (namespace == 'URL') namespace = uuidV5.URL;
if (namespace == 'DNS') namespace = uuidV5.DNS;
console.log(uuidV5(name, namespace));
break;
default:
usage();
process.exit(1);
}

View File

@@ -11,8 +11,8 @@
}, },
"nodemonConfig": { "nodemonConfig": {
"ignore": [ "ignore": [
"./data/dataId.json", "./data/messageData.json",
"./data/threads.json" "./logs/logs.json"
], ],
"delay": "10" "delay": "10"
} }

View File

@@ -1,6 +1,6 @@
[Service] [Service]
User=root User=root
ExecStart=node /home/user/telegram_support_bot/telegram_support_bot.js ExecStart=node /root/telegram_support_bot/telegram_support_bot.js
Restart=always Restart=always
[Install] [Install]

View File

@@ -1,14 +1,16 @@
const {Telegraf, Markup} = require('telegraf'); const {Telegraf, Markup} = require('telegraf');
const glpm = require('./lib/glpm.js'); const glpm = require('./lib/glpm.js');
const cns = require('./lib/const.js'); const cns = require('./lib/const.js');
const {sleep, parseMessageText, htmlToText} = require('./lib/utils.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 fs = require('fs');
const dir = __dirname; const dir = __dirname;
let ticketData = {}; let ticketData = {};
const conf = JSON.parse(fs.readFileSync(dir + "/data/conf.json")); let conf = JSON.parse(fs.readFileSync(dir + "/data/conf.json"));
const glpiUrl = conf.glpiConfig.apiurl.replace("apirest.php", ""); const glpiUrl = conf.glpiConfig.apiurl.replace("apirest.php", "");
let threadsData = JSON.parse(fs.readFileSync(dir + "/data/threads.json")); let messageData = JSON.parse(fs.readFileSync(dir + "/data/messageData.json"));
let configData = {};
const bot = new Telegraf(conf.telegramBotToken, { const bot = new Telegraf(conf.telegramBotToken, {
handlerTimeout: 90000 * 5 handlerTimeout: 90000 * 5
@@ -27,7 +29,19 @@ bot.hears('Подать заявку', async (ctx) => {
await ctx.reply('Выберите, пожалуйста, наиболее подходящую категорию', cns.keyboards.main); await ctx.reply('Выберите, пожалуйста, наиболее подходящую категорию', cns.keyboards.main);
await deleteMessage(ctx, ctx.message.message_id); await deleteMessage(ctx, ctx.message.message_id);
} }
}).catch(error => console.log(error)); });
// Настройка кнопок для переназначения заявки
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);
}
});
// Создаём кнопки основного меню // Создаём кнопки основного меню
@@ -67,7 +81,7 @@ bot.hears('Назад', async (ctx) => {
}catch{ }catch{
ctx.reply('Добро пожаловать в чат-бот технической поддержки ' + conf.CompanyName, cns.keyboards.start); ctx.reply('Добро пожаловать в чат-бот технической поддержки ' + conf.CompanyName, cns.keyboards.start);
} }
}).catch(error => console.log(error)); });
bot.hears('Отменить заявку', async (ctx) => { bot.hears('Отменить заявку', async (ctx) => {
if(ctx.chat.id != conf.supportChatId){ if(ctx.chat.id != conf.supportChatId){
@@ -76,7 +90,7 @@ bot.hears('Отменить заявку', async (ctx) => {
await deleteMessage(ctx, ctx.message.message_id - 1); await deleteMessage(ctx, ctx.message.message_id - 1);
await deleteMessage(ctx, ctx.message.message_id); await deleteMessage(ctx, ctx.message.message_id);
} }
}).catch(error => console.log(error)); });
bot.hears('Отправить заявку', async (ctx) => { 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.id != conf.supportChatId && ticketData.hasOwnProperty(ctx.chat.id) && ticketData[ctx.chat.id].data["Кабинет"].length > 1){
@@ -90,9 +104,8 @@ bot.hears('Отправить заявку', async (ctx) => {
for(key in ticketData[ctx.chat.id]['data']){ for(key in ticketData[ctx.chat.id]['data']){
textToGLPI += '<b>' + key + ':</b> ' + ticketData[ctx.chat.id]['data'][key].replace(/[<>/]/g, '') + '<br>'; textToGLPI += '<b>' + key + ':</b> ' + ticketData[ctx.chat.id]['data'][key].replace(/[<>/]/g, '') + '<br>';
} }
let res = await glpm.createTicket(title, textToGLPI); let ticketId = await glpm.createTicket(title, textToGLPI);
let messageText = `<a href="http://example.com/${ctx.chat.id}/${ctx.message.message_id+2}">&#8203</a>`; let messageText = `🟢 <b>ЗАЯВКА <a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">№${ticketId}</a></b>\n\n`;
messageText += `🟢 <b>ЗАЯВКА <a href="${glpiUrl}front/ticket.form.php?id=${res}">№${res}</a></b>\n\n`;
let userLogin = ''; let userLogin = '';
if(ctx.chat.username) userLogin = ' (@' + ctx.chat.username + ')'; if(ctx.chat.username) userLogin = ' (@' + ctx.chat.username + ')';
ticketData[ctx.chat.id]['data']['Автор заявки'] += userLogin; ticketData[ctx.chat.id]['data']['Автор заявки'] += userLogin;
@@ -103,17 +116,25 @@ bot.hears('Отправить заявку', async (ctx) => {
parse_mode: 'HTML', parse_mode: 'HTML',
reply_markup: {inline_keyboard: cns.inlineKeyboards.open} 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 ctx.reply('Заявка отправлена', cns.keyboards.start);
await deleteMessage(ctx, ctx.message.message_id - 1); await deleteMessage(ctx, ctx.message.message_id - 1);
await deleteMessage(ctx, ctx.message.message_id); await deleteMessage(ctx, ctx.message.message_id);
let messageUserText = messageText.replace('">&#8203</a>', `/${messg.message_id}">&#8203</a>`); await ctx.telegram.sendMessage(ctx.chat.id, messageText, {
await ctx.telegram.sendMessage(ctx.message.chat.id, messageUserText, {
parse_mode: 'HTML', parse_mode: 'HTML',
reply_markup: {inline_keyboard: cns.inlineKeyboards.userAddComment} reply_markup: {inline_keyboard: cns.inlineKeyboards.userAddComment}
}); });
fs.writeFileSync(dir + "/data/messageData.json", JSON.stringify(messageData, null, 3));
delete ticketData[ctx.chat.id]; delete ticketData[ctx.chat.id];
} }
}).catch(error => console.log(error)); });
// Обработка сообщений // Обработка сообщений
@@ -124,13 +145,17 @@ bot.on('text', async (ctx) => {
ticketId = ctx.message.reply_to_message.text.split('№')[1].split('\n')[0]; ticketId = ctx.message.reply_to_message.text.split('№')[1].split('\n')[0];
}catch{} }catch{}
if(!ticketData.hasOwnProperty(ctx.chat.id) && ticketId){ if(!ticketData.hasOwnProperty(ctx.chat.id) && ticketId){
if(!threadsData.hasOwnProperty(ticketId)){ if(!messageData.data[ticketId].hasOwnProperty('threadId')){
let generalMessageId = ctx.message.reply_to_message.entities[0].url.split('/')[5]; let ticket = await glpm.getItem('Ticket', ticketId);
await createThread(ticketId, generalMessageId, ctx.chat.id, ctx.message.reply_to_message.message_id); let title = `🟢 ${ticketId} - ${ticket.name}`;
await createThread(bot, messageData, ticketId, title);
} }
if(!ctx.chat.last_name) ctx.chat.last_name = '';
let userName = ctx.chat.first_name + ' ' + ctx.chat.last_name; let userName = ctx.chat.first_name + ' ' + ctx.chat.last_name;
let messageText = `<b>Комментарий от ${userName}:</b>\n\n${ctx.message.text}`; let messageText = `<b>Комментарий от ${userName}:</b>\n\n${ctx.message.text}`;
await bot.telegram.sendMessage(conf.supportChatId, messageText, {parse_mode: "HTML", message_thread_id: threadsData[ticketId]["threadId"]}); await bot.telegram.sendMessage(conf.supportChatId, messageText, {
parse_mode: "HTML", message_thread_id: messageData.data[ticketId].threadId
});
let discript = `<b><font color="blue">Комментарий от ${userName}:</font></b><br><br>`; let discript = `<b><font color="blue">Комментарий от ${userName}:</font></b><br><br>`;
await glpm.addComment(ticketId, discript + ctx.message.text); await glpm.addComment(ticketId, discript + ctx.message.text);
}else if(ticketId){ }else if(ticketId){
@@ -155,40 +180,56 @@ bot.on('text', async (ctx) => {
await deleteMessage(ctx, ctx.message.message_id); await deleteMessage(ctx, ctx.message.message_id);
} }
}else if(ctx.message.message_thread_id){ }else if(ctx.message.message_thread_id){
let thread = {};
let ticket; let ticket;
for(let i in threadsData){ for(let i in messageData.data){
if(threadsData[i].threadId == ctx.message.message_thread_id){ if(messageData.data[i].threadId == ctx.message.message_thread_id){
thread = threadsData[i];
ticket = i; ticket = i;
break; break;
} }
} }
if(thread.hasOwnProperty('threadId') && thread.userChatId){ if(messageData.data[ticket].hasOwnProperty('userChatId')){
await bot.telegram.sendMessage(thread.userChatId, ctx.message.text, {reply_parameters: {message_id: thread.userMessgId}}); 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); await glpm.addComment(ticket, ctx.message.text);
}else if(configData.hasOwnProperty("flag")){
configData["text"] = ctx.message.text.trim();
configData["id"] = ctx.message.message_id;
} }
}).catch(error => console.log(error)); });
// Обработка изображений из приватных чатов // Обработка изображений из приватных чатов
bot.on('photo', async (ctx) => { bot.on('photo', async (ctx) => {
if(ctx.chat.id != conf.supportChatId){ if(ctx.chat.id != conf.supportChatId){
if(!ticketData.hasOwnProperty(ctx.chat.id)){
let ticketId; let ticketId;
let messg = ctx.message.reply_to_message.text;
try{ try{
ticketId = ctx.message.reply_to_message.text.split('№')[1].split('\n')[0]; ticketId = messg.split('№')[1].split('\n')[0];
}catch{} }catch{}
if(!ticketId) return; if(!ticketData.hasOwnProperty(ctx.chat.id) && ticketId){
if(!threadsData.hasOwnProperty(ticketId)){ if(!messageData.data[ticketId].hasOwnProperty(threadId)){
let generalMessageId = ctx.message.reply_to_message.entities[0].url.split('/')[5]; let ticket = await glpm.getItem('Ticket', ticketId);
await createThread(ticketId, generalMessageId, ctx.chat.id, ctx.message.reply_to_message.message_id); 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: threadsData[ticketId]["threadId"]}); await bot.telegram.sendPhoto(conf.supportChatId, ctx.message.photo[0].file_id, {message_thread_id: messageData.data[ticketId]["threadId"]});
} }
} }
}).catch(error => console.log(error)); });
// Сохраняем ID последнего сообщения в чате
bot.on('message', async (ctx) => {
if(ctx.message.forum_topic_edited){
await deleteMessage(ctx, ctx.message.message_id);
}
});
// Функция для создания кнопок основного меню с похожим функционалом // Функция для создания кнопок основного меню с похожим функционалом
@@ -218,71 +259,60 @@ async function deleteMessage(ctx, message_id){
}catch{} }catch{}
} }
async function editMessage(message, keyboard, silent){
try{
let ticketId = message.text.split('\n')[0].split('№')[1];
let chatId = message.chat.id;
let ticketData = await glpm.getItem('Ticket', ticketId);
message['status'] = ticketData.status;
if(message.entities[0].url.split('/')[5]) silent = undefined;
messageText = await parseMessageText(message, silent);
let inKeyboard = JSON.parse(JSON.stringify(keyboard));
if(inKeyboard[0][0].text == "✔" || inKeyboard[0][0].text == '✅'){
if(threadsData.hasOwnProperty(ticketId)){
delete inKeyboard[0][1].callback_data;
inKeyboard[0][1]["url"] = `t.me/c/${conf.supportChatId.substring(4)}/${threadsData[ticketId].threadId}`;
}else{
delete inKeyboard[0][1].url;
inKeyboard[0][1].callback_data = 'AddComment';
}
}
await bot.telegram.editMessageText(chatId, message.message_id, undefined, messageText, {
parse_mode: 'HTML',
reply_markup: {inline_keyboard: inKeyboard},
entities: message.entities
});
}catch{}
}
// Создаём обработчики инлайн кнопок // Создаём обработчики инлайн кнопок
createAction('ConfirmOpen', 1, cns.inlineKeyboards.open); createAction('OpenTicket', cns.inlineKeyboards.confirmOpen);
createAction('ConfirmClose', 6, cns.inlineKeyboards.close); createAction('CloseTicket', cns.inlineKeyboards.confirmClose);
createAction('OpenTicket', null, cns.inlineKeyboards.confirmOpen); createAction('ChangeStatus', cns.inlineKeyboards.changeStatus);
createAction('CloseTicket', null, cns.inlineKeyboards.confirmClose); createAction('ConfirmOpen', cns.inlineKeyboards.open, 1);
createAction('CloseThread', null, cns.inlineKeyboards.threadPinConfirm); createAction('ConfirmClose', cns.inlineKeyboards.close, 6);
createAction('CancelCloseThread', null, cns.inlineKeyboards.threadPin); createAction('WaitingStatus', cns.inlineKeyboards.open, 4);
createAction('WorkingStatus', cns.inlineKeyboards.open, 2);
createAction('OpenThread', cns.inlineKeyboards.open);
function createAction(action, status, keyboard){ function createAction(action, keyboard, status){
try{ try{
bot.action(action, async (ctx) => { bot.action(action, async (ctx) => {
let message = ctx.update.callback_query.message; let message = ctx.update.callback_query.message;
if(status){
let ticketId = message.text.split('\n')[0].split('№')[1]; let ticketId = message.text.split('\n')[0].split('№')[1];
let td = messageData.data[ticketId];
if(status || action == 'OpenThread'){
await glpm.changeStatusTicket(ticketId, status); await glpm.changeStatusTicket(ticketId, status);
if(dataId.hasOwnProperty(ticketId)) dataId[ticketId].status = status; await editTicketStatus(bot, messageData, message);
if(status == 6){ if(status == 6){
let userChatId = message.entities[0].url.split('/')[3]; if(td.hasOwnProperty('userChatId')){
let userMessgId = message.entities[0].url.split('/')[4]; try{
if(userChatId){ await bot.telegram.sendMessage(td.userChatId, "<b>Заявка закрыта</b>", {
await bot.telegram.sendMessage(userChatId, "<b>Заявка закрыта</b>", { reply_parameters: {message_id: td.userMassageId},
reply_parameters: {message_id: userMessgId}, parse_mode: "HTML"
});
}catch{
await bot.telegram.sendMessage(td.userChatId, "<b>Заявка закрыта</b>", {
parse_mode: "HTML" parse_mode: "HTML"
}); });
} }
if(threadsData.hasOwnProperty(ticketId)){
let thread = threadsData[ticketId].threadId;
await bot.telegram.deleteForumTopic(conf.supportChatId, thread);
delete threadsData[ticketId];
let jsonData = JSON.stringify(threadsData, null, 3);
fs.writeFileSync(dir + "/data/threads.json", jsonData);
} }
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);
} }
await editMessage(message, keyboard);
}); });
}catch(error){ }catch(e){
console.error('Failed to action: ' + action, error); fs.appendFileSync(dir + "/logs/logs.json", JSON.stringify(e, null, 3));
} }
} }
@@ -290,80 +320,102 @@ function createAction(action, status, keyboard){
bot.action('RefreshStatus', async (ctx) => { bot.action('RefreshStatus', async (ctx) => {
let message = ctx.update.callback_query.message; let message = ctx.update.callback_query.message;
let ticketId = message.text.split('\n')[0].split('№')[1]; await editTicketStatus(bot, messageData, message);
let ticketData = await glpm.getItem('Ticket', ticketId); });
switch(ticketData.status){
case 1: await editMessage(message, cns.inlineKeyboards.open); break;
case 2: await editMessage(message, cns.inlineKeyboards.open); break;
case 4: await editMessage(message, cns.inlineKeyboards.open); break;
case 6: await editMessage(message, cns.inlineKeyboards.close); break;
default: await editMessage(message, cns.inlineKeyboards.close); break;
}
}).catch(error => console.log(error));
// Обработчик для кнопки с облачком, которая создает новую тему // Обработчик для кнопки с подсказкой о добавлении комментария
bot.action('AddComment', async (ctx) => {
let message = ctx.update.callback_query.message;
let userChatId = message.entities[0].url.split('/')[3];
let userMessgId = message.entities[0].url.split('/')[4];
let generalMessageId = message.entities[0].url.split('/')[5];
if(!generalMessageId) generalMessageId = message.message_id;
let ticketId = message.text.split('\n')[0].split('№')[1];
if(!threadsData.hasOwnProperty(ticketId)){
await editMessage(message, message.reply_markup.inline_keyboard, generalMessageId);
await createThread(ticketId, generalMessageId, userChatId, userMessgId);
}
}).catch(error => console.log(error));
// Создание новой темы
async function createThread(ticketId, generalMessageId, userChatId, userMessageId){
let thread = await bot.telegram.createForumTopic(conf.supportChatId, ticketId);
threadsData[ticketId] = {
"userChatId": userChatId,
"userMessgId": userMessageId,
"threadId": thread.message_thread_id
};
let ticket = await glpm.getItem('Ticket', ticketId);
let inKeyboard = JSON.parse(JSON.stringify(cns.inlineKeyboards.open));
if (ticket.status == 6 || ticket.status == 5){
inKeyboard = JSON.parse(JSON.stringify(cns.inlineKeyboards.close));
}
delete inKeyboard[0][1].callback_data;
inKeyboard[0][1]["url"] = `t.me/c/${conf.supportChatId.substring(4)}/${thread.message_thread_id}`;
await bot.telegram.editMessageReplyMarkup(conf.supportChatId, generalMessageId, undefined, { inline_keyboard: inKeyboard });
let msg = await bot.telegram.copyMessage(conf.supportChatId, conf.supportChatId, generalMessageId, {
parse_mode: 'HTML',
disable_notification: true,
message_thread_id: thread.message_thread_id,
reply_markup: { inline_keyboard: cns.inlineKeyboards.threadPin }
});
await bot.telegram.pinChatMessage(conf.supportChatId, msg.message_id, { disable_notification: true });
let jsonData = JSON.stringify(threadsData, null, 3);
fs.writeFileSync(dir + "/data/threads.json", jsonData);
}
bot.action('UserAddComment', async (ctx) => { bot.action('UserAddComment', async (ctx) => {
await ctx.reply('Чтобы отправить комментарий, <b>ответьте на сообщение с номером заявки</b> (которое начинается с 🟢)', {parse_mode: "HTML"}); await ctx.reply('Чтобы отправить комментарий, <b>ответьте на сообщение с номером заявки</b> (которое начинается с 🟢)', {parse_mode: "HTML"});
}); });
// Закрыть тему // Изменить конфигурацию пользовательских групп
bot.action('ConfirmCloseThread', async (ctx) => { 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());
conf.userGroups[rgu[0].trim()].splice(index, 1); break;
}
let jsonData = JSON.stringify(conf, null, 3);
fs.writeFileSync(dir + "/data/conf.json", jsonData);
createAssignActions();
await deleteMessage(ctx.update.callback_query, 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: 'RefreshStatus'});
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 message = ctx.update.callback_query.message;
let ticketId = message.text.split('№')[1].split('\n')[0]; let ticketId = message.text.split('№')[1].split('\n')[0];
let messageId = message.entities[0].url.split('/')[5]; for(let i in conf.userGroups[key]){
message.message_id = messageId; await glpm.assignTicket(ticketId, conf.userGroups[key][i]);
delete threadsData[ticketId]; }
await bot.telegram.deleteForumTopic(conf.supportChatId, message.message_thread_id); if(message.text.indexOf("⚫") < 0){
let ticket = await glpm.getItem("Ticket", ticketId); await glpm.changeStatusTicket(ticketId, 2);
let inKeyboard = cns.inlineKeyboards.open; }
if(ticket.status == 6 || ticket.status == 5) inKeyboard = cns.inlineKeyboards.close; await editTicketStatus(bot, messageData, message);
await editMessage(message, inKeyboard); if(messageData.data[ticketId].hasOwnProperty('threadId')){
let jsonData = JSON.stringify(threadsData, null, 3); await closeThread(bot, messageData, ticketId);
fs.writeFileSync(dir + "/data/threads.json", jsonData); }
}).catch(error => console.log(error)); });
}
}
process.on('uncaughtException', (error) => { process.on('uncaughtException', (error) => {
console.log(error); console.log(error);
@@ -373,163 +425,21 @@ bot.launch();
// Сборщик заявок и комментариев из GLPI // Сборщик заявок и комментариев из GLPI
let dataId = JSON.parse(fs.readFileSync(dir + "/data/dataId.json"));
(async () => { (async () => {
let counter = 0; // счетчик для выполнения функции refreshStatus() let counter = 0; // счетчик для выполнения функции refreshStatus()
while (true) { while (true) {
try{ try{
await parseTickets(bot, messageData);
// Собираем заявки await parseComments(bot, messageData);
let listTickets = await glpm.getAllItems('Ticket', 4);
for(let i = 5; i >= 0; i--){
let ticketId;
if(!listTickets) break;
if(!listTickets[i]) continue;
ticketId = listTickets[i].id;
if(ticketId <= dataId.ticket) continue;
if(listTickets[i].users_id_recipient != conf.glpiConfig.user_id){
let usersArray = await glpm.getUsers(ticketId);
let authorEmail;
if(!usersArray[0]) continue;
if(usersArray[0].hasOwnProperty('alternative_email') && usersArray[0].alternative_email){
authorEmail = usersArray[0].alternative_email;
}else{
let temp = await glpm.getItem("User", usersArray[0].users_id);
authorEmail = temp.firstname + ' ' + temp.realname;
}
let text = await htmlToText(listTickets[i].content);
let messageText = `🟢 <b>ЗАЯВКА <a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">№${ticketId}</a></b>\n\n`;
messageText += `<b>Автор заявки: </b>${authorEmail}\n`;
messageText += `<b>Проблема: </b>${listTickets[i].name}\n<b>Описание: </b>`;
messageText += text;
if(messageText.length > 600){
messageText = `${messageText.substring(0, 500)} + '\n\n<b><a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">Читать дальше</a></b>`;
}
let messg = await bot.telegram.sendMessage(conf.supportChatId, messageText, {
parse_mode: 'HTML',
reply_markup: { inline_keyboard: cns.inlineKeyboards.open }
});
let silentInfo = `<a href="http://example.com///${messg.message_id}">&#8203</a>`;
await bot.telegram.editMessageText(conf.supportChatId, messg.message_id, undefined, silentInfo + messageText, {
parse_mode: 'HTML',
reply_markup: { inline_keyboard: cns.inlineKeyboards.open }
});
dataId.history[ticketId] = {};
dataId.history[ticketId]["messageId"] = messg.message_id;
dataId.history[ticketId]["status"] = 1;
dataId.ticket = ticketId;
}
fs.writeFileSync(dir + "/data/dataId.json", JSON.stringify(dataId, null, 3));
}
// Собираем комментарии
let listComments = await glpm.getAllItems('ITILFollowup', 4);
for (let i = 5; i >= 0; i--) {
if(!listComments) break;
if(!listComments[i]) continue;
let commentId = listComments[i].id;
if (commentId <= dataId.comment) continue;
if (listComments[i].users_id != conf.glpiConfig.user_id){
let ticketId = listComments[i].items_id;
let text = await htmlToText(listComments[i].content);
let user;
if (listComments[i].users_id) {
let temp = await glpm.getItem("User", listComments[i].users_id);
user = temp.firstname + ' ' + temp.realname;
} else {
let temp = await glpm.getUsers(ticketId);
user = temp[0].alternative_email;
}
await addComment(text, ticketId, user);
}
dataId.comment = commentId;
fs.writeFileSync(dir + "/data/dataId.json", JSON.stringify(dataId, null, 3));
}
}catch(err){ console.log(err) }
await sleep(10000);
counter++;
if(counter >= 60){ if(counter >= 60){
await refreshStatus(); await refreshStatus(bot, messageData);
counter = 0; counter = 0;
} }
}catch(e){
fs.appendFileSync(dir + "/logs/logs.json", JSON.stringify(e, null, 3));
}
await sleep(10000);
counter++;
} }
})(); })();
// Отправка комментария в Telegram
async function addComment(comment, ticketId, user){
try{
threadsData = JSON.parse(fs.readFileSync(dir + "/data/threads.json"));
if(!threadsData.hasOwnProperty(ticketId)){
if(!dataId.history.hasOwnProperty(ticketId)) return;
let generalMessageId = dataId.history[ticketId].messageId;
await createThread(ticketId, generalMessageId);
}
let messageText = `<b>Комментарий от ${user}:</b>\n\n${comment}`;
if(messageText.length > 2400){
messageText = `${messageText.substring(0, 2400)} + '\n\n<b><a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">Читать дальше</a></b>`;
}
await bot.telegram.sendMessage(conf.supportChatId, messageText, {parse_mode: "HTML", message_thread_id: threadsData[ticketId]["threadId"]});
}catch(err){console.log(err)}
}
// обновляет статусы последних 50 заявок
async function refreshStatus(){
let listTickets = await glpm.getAllItems('Ticket', 49);
dataId = JSON.parse(fs.readFileSync(dir + "/data/dataId.json"));
for(let i = 50; i >= 0; i--){
if(!listTickets) break;
if(!listTickets[i]) continue;
let ticketId = listTickets[i].id;
try{
if(dataId["history"][ticketId].status != listTickets[i].status && listTickets[i].users_id_recipient != conf.glpiConfig.user_id){
let messageId = dataId["history"][ticketId].messageId;
let color = '🟢';
switch(listTickets[i].status){
case 1: color = '🟢'; break;
case 2: color = '🔵'; break;
case 4: color = '🟠'; break;
case 6: color = '⚫'; break;
default: color = '⚫';
}
let usersArray = await glpm.getUsers(ticketId);
let authorEmail;
if(usersArray[0].hasOwnProperty('alternative_email') && usersArray[0].alternative_email){
authorEmail = usersArray[0].alternative_email;
}else{
let temp = await glpm.getItem("User", usersArray[0].users_id);
authorEmail = temp.firstname + ' ' + temp.realname;
}
let text = await htmlToText(listTickets[i].content);
let messageText = `${color} <b>ЗАЯВКА <a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">№${ticketId}</a></b>\n\n`;
messageText += `<b>Автор заявки: </b>${authorEmail}\n`;
messageText += `<b>Проблема: </b>${listTickets[i].name}\n<b>Описание: </b>`;
messageText += text;
if(messageText.length > 600){
messageText = `${messageText.substring(0, 500)} + '\n\n<b><a href="${glpiUrl}front/ticket.form.php?id=${ticketId}">Читать дальше</a></b>`;
}
let silentInfo = `<a href="http://example.com///${messageId}">&#8203</a>`;
let inKeyboard;
if(listTickets[i].status == 5 || listTickets[i].status == 6){
inKeyboard = JSON.parse(JSON.stringify(cns.inlineKeyboards.close));
}else{
inKeyboard = JSON.parse(JSON.stringify(cns.inlineKeyboards.open));
}
if(threadsData.hasOwnProperty(ticketId)){
delete inKeyboard[0][1].callback_data;
inKeyboard[0][1]["url"] = `t.me/c/${conf.supportChatId.substring(4)}/${threadsData[ticketId].threadId}`;
}
await bot.telegram.editMessageText(conf.supportChatId, messageId, undefined, silentInfo + messageText, {
parse_mode: 'HTML',
reply_markup: { inline_keyboard: inKeyboard }
});
dataId.history[ticketId]["status"] = listTickets[i].status;
}
}catch(err){}
}
fs.writeFileSync(dir + "/data/dataId.json", JSON.stringify(dataId, null, 3));
}