copied the code from the working repo

This commit is contained in:
2024-11-30 16:00:48 +03:00
parent f22b92869b
commit 15ac0cb9b8
148 changed files with 23342 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
**/node_modules
*.md
*.env
Dockerfile
docker-compose.yml
**/.npmignore
**/.dockerignore
**/*.md
**/*.log
**/.vscode
**/.git
**/.eslintrc.json
*.sh

View File

@@ -0,0 +1,9 @@
BOT_TOKEN=
DB_NAME=
DB_HOST=
DB_USER=
DB_PASSWORD=
PORT=
HOOKPORT=
DOMAIN=
WEB_APP=

4
mtucijobsbot/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.DS_store
**/.DS_Store
node_modules
.env

12
mtucijobsbot/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM node:21.5.0-alpine3.19
ENV NEXT_TELEMETRY_DISABLED=1
WORKDIR /app
COPY package*.json ./
RUN npm i
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]

5
mtucijobsbot/build.sh Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
docker build -f Dockerfile . \
-t mtuci-jobs-image:latest \
--compress \
--force-rm

5
mtucijobsbot/deploy.sh Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
git pull
docker-compose -f docker-compose.yml up -d --build
docker exec -t bot-mtuci-jobs ash -c "NODE_ENV=production npx sequelize-cli db:migrate"
docker-compose -f docker-compose.yml logs --tail=10 -f

40
mtucijobsbot/dist/api/resume.js vendored Normal file
View File

@@ -0,0 +1,40 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
require("dotenv/config");
const router = express_1.default.Router();
router.post('/api/resume', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
try {
const postData = req.body;
// Проверяем наличие chatId в теле запроса
if (!postData.chatId) {
throw new Error('Chat ID is missing in the request body');
}
// Ваша логика обработки данных
console.log('Received data:', postData.id);
// Отправка сообщения в боте
// await req.bot.telegram.sendMessage(
// postData.chatId,
// `Received data: ${postData.id}`
// );
// MessagesService.sendMessage(postData.id, )
res.status(200).json({ message: 'Data received successfully' });
}
catch (error) {
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
}
}));
exports.default = router;

Binary file not shown.

90
mtucijobsbot/dist/commands/start.js vendored Normal file
View File

@@ -0,0 +1,90 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startCommand = void 0;
const db_1 = require("../db/db");
const User_1 = require("../models/User");
const axios_1 = __importDefault(require("axios"));
function startCommand(ctx) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
function isBotOwner(userId) {
const ownerUserId = 1053404914; // Замените этот ID на фактический ID владельца
return userId === ownerUserId;
}
try {
yield db_1.sequelize.authenticate(); // Проверка подключения к базе данных
yield db_1.UserBase.sync(); // Создание таблицы, если её нет
console.log('User table has been created or already exists.');
// Проверка, существует ли пользователь в базе данных
const [userInstance, created] = yield db_1.UserBase.findOrCreate({
where: { id: (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id },
defaults: {
id: ((_b = ctx.from) === null || _b === void 0 ? void 0 : _b.id) || 0,
username: ((_c = ctx.from) === null || _c === void 0 ? void 0 : _c.username) || '',
vacansyindex: 0,
role: isBotOwner((_d = ctx.from) === null || _d === void 0 ? void 0 : _d.id) ? User_1.UserRole.OWNER : User_1.UserRole.CLIENT,
language: ((_e = ctx.from) === null || _e === void 0 ? void 0 : _e.language_code) || 'ru',
message_id: ((_g = (_f = ctx.callbackQuery) === null || _f === void 0 ? void 0 : _f.message) === null || _g === void 0 ? void 0 : _g.message_id) || 0,
last_obj: '',
chat_id: (_h = ctx.chat) === null || _h === void 0 ? void 0 : _h.id,
resume_id: null,
},
});
// Выполняем запрос на удаление резюме, если оно существует
try {
const response = yield axios_1.default.delete(`${process.env.API}students/${(_j = ctx.from) === null || _j === void 0 ? void 0 : _j.id}`, {
headers: {
'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
});
if (response.status === 204) {
console.log('Резюме успешно удалено на сервере.');
yield db_1.Resume.destroy({ where: { id: (_k = ctx.from) === null || _k === void 0 ? void 0 : _k.id } }); // Удаляем запись из базы данных
yield ctx.reply('Ваше резюме было удалено.');
}
else {
console.warn('Не удалось удалить резюме, сервер вернул статус:', response.status);
yield ctx.reply('Не удалось удалить ваше резюме. Попробуйте позже.');
}
}
catch (error) {
console.error('Ошибка при удалении резюме:', error);
yield ctx.reply('Произошла ошибка при удалении резюме. Пожалуйста, попробуйте позже.');
}
// Приветственное сообщение
const greetingMessage = {
text: 'Вас приветствует бот MTUCI jobs! \n\n' +
'Для подтверждения того, что вы студент МТУСИ, вам потребуется привязать вашу учетную запись LMS к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты.',
buttons: [[{ text: 'Понял', callback: 'accept2' }]],
};
// Отправка приветственного сообщения с кнопками
yield ctx.reply(greetingMessage.text, {
reply_markup: {
inline_keyboard: greetingMessage.buttons.map(row => row.map(button => ({
text: button.text,
callback_data: button.callback,
}))),
},
parse_mode: 'HTML',
});
console.log('Бот запущен');
}
catch (e) {
console.error('Произошла ошибка при запуске бота', e);
ctx.reply('Произошла ошибка при запуске. Пожалуйста, попробуйте позже.');
}
});
}
exports.startCommand = startCommand;

109
mtucijobsbot/dist/db/db.js vendored Normal file
View File

@@ -0,0 +1,109 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Resume = exports.UserBase = exports.sequelize = void 0;
require("dotenv/config");
const sequelize_1 = require("sequelize");
const sequelize = new sequelize_1.Sequelize(process.env.DB_NAME || 'custdev', process.env.DB_USER || 'postgres', process.env.DB_PASSWORD, {
host: process.env.DB_HOST,
dialect: 'postgres',
});
exports.sequelize = sequelize;
class UserBase extends sequelize_1.Model {
}
exports.UserBase = UserBase;
UserBase.init({
id: {
type: sequelize_1.DataTypes.BIGINT,
primaryKey: true,
},
username: {
type: sequelize_1.DataTypes.STRING,
allowNull: false,
},
vacansyindex: {
type: sequelize_1.DataTypes.INTEGER,
allowNull: false,
},
role: {
type: sequelize_1.DataTypes.STRING,
allowNull: false,
defaultValue: 'client',
},
language: {
type: sequelize_1.DataTypes.STRING,
allowNull: false,
},
message_id: {
type: sequelize_1.DataTypes.INTEGER,
allowNull: true,
},
last_obj: {
type: sequelize_1.DataTypes.STRING,
allowNull: true,
},
chat_id: {
type: sequelize_1.DataTypes.BIGINT,
allowNull: true,
},
resume_id: {
type: sequelize_1.DataTypes.BIGINT,
allowNull: true,
references: {
model: 'Resumes', // имя таблицы резюме
key: 'id',
},
},
}, {
sequelize,
modelName: 'User',
});
class Resume extends sequelize_1.Model {
}
exports.Resume = Resume;
Resume.init({
id: {
type: sequelize_1.DataTypes.BIGINT,
primaryKey: true,
},
name: {
type: sequelize_1.DataTypes.STRING,
allowNull: false,
},
group: {
type: sequelize_1.DataTypes.STRING,
allowNull: false,
},
type: {
type: sequelize_1.DataTypes.STRING,
allowNull: false,
},
skills: {
type: sequelize_1.DataTypes.STRING,
allowNull: false,
},
softskills: {
type: sequelize_1.DataTypes.STRING,
allowNull: false,
},
email: {
type: sequelize_1.DataTypes.STRING,
allowNull: false,
},
resumefile: {
type: sequelize_1.DataTypes.STRING,
allowNull: false,
},
}, {
sequelize,
modelName: 'Resume',
});
UserBase.belongsTo(Resume, { foreignKey: 'resume_id', as: 'resume' });
Resume.hasOne(UserBase, { foreignKey: 'resume_id', as: 'user' });
sequelize
.sync()
.then(() => {
console.log('Model has been synchronized successfully.');
})
.catch(error => {
console.error('Error synchronizing model:', error);
});

375
mtucijobsbot/dist/index.js vendored Normal file
View File

@@ -0,0 +1,375 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
require("dotenv/config");
const telegraf_1 = require("telegraf");
const start_1 = require("./commands/start");
const menuScenes_1 = require("./modules/menuScenes");
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const events_1 = require("./modules/scenes/events");
const form_data_1 = __importDefault(require("form-data"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const axios_1 = __importDefault(require("axios"));
const savevacansy_1 = require("./modules/scenes/savevacansy");
const accept_1 = require("./modules/scenes/accept");
const app = (0, express_1.default)();
const bot = new telegraf_1.Telegraf(process.env.BOT_TOKEN);
app.use((req, res, next) => {
req.bot = bot;
next();
});
app.use(express_1.default.json());
app.use((0, cors_1.default)());
bot.start(start_1.startCommand);
app.post('/api/resume', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
try {
const postData = req.body;
const resumeTemplatePath = path_1.default.resolve(__dirname, 'assets', 'шаблон МТУСИ.docx');
yield bot.telegram.sendDocument(postData.id, { source: resumeTemplatePath }, { caption: 'Пример резюме' });
yield bot.telegram.sendMessage(postData.id, `Ваша анкета заполнена! Отправьте своё резюме для завершения регистрации`, {
reply_markup: {
inline_keyboard: [
[
{ text: 'Загрузить резюме', callback_data: 'uploadresume' },
// { text: 'Пропустить загрузку', callback_data: 'skip' },
],
],
},
});
console.log(`Ваш id ${postData.id}`);
return res.json({ status: 'success' });
}
catch (e) {
console.error('Error:', e);
res.status(500).json({ error: 'An error occurred' });
throw e;
}
}));
// const resumeScene: Scenes.WizardScene<MyWizardContext> = new Scenes.WizardScene(
// 'resumeScene',
// async ctx => {
// await ctx.reply(
// 'Отправьте своё резюме в PDF формате или отправьте /cancel для отмены.'
// );
// return ctx.wizard.next();
// },
// async ctx => {
// if (
// ctx.message &&
// 'text' in ctx.message &&
// ctx.from &&
// ctx.message.text == '/cancel'
// ) {
// await ctx.reply('Отправка резюме отменена.');
// await ctx.reply(
// 'Меню',
// Markup.keyboard([
// ['Моя анкета'],
// ['Вакансии'],
// ['Уведомления: включены'],
// ]).resize()
// );
// return ctx.scene.leave();
// }
// if (ctx.message && 'document' in ctx.message && ctx.from) {
// const file = ctx.message.document;
// if (file.mime_type !== 'application/pdf') {
// ctx.reply('Пожалуйста, отправьте файл в формате PDF.');
// return;
// }
// try {
// const fileLink = await ctx.telegram.getFileLink(file.file_id);
// const filePath = path.join(__dirname, `${file.file_id}.pdf`);
// // Загрузка файла на локальную машину
// const response = await axios.get(fileLink.href, {
// responseType: 'stream',
// });
// response.data
// .pipe(fs.createWriteStream(filePath))
// .on('finish', async () => {
// // Создание формы данных
// const form = new FormData();
// form.append('file', fs.createReadStream(filePath));
// try {
// // Установка времени ожидания (в миллисекундах)
// const uploadResponse = await axios.post(
// `${process.env.API}services/resume/`,
// form,
// {
// headers: {
// ...form.getHeaders(),
// 'X-API-KEY':
// 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
// },
// timeout: 10000, // Увеличьте время ожидания до 10 секунд
// }
// );
// const fileName = uploadResponse.data.filename;
// // Выполнение PUT-запроса для обновления поля Link у студента
// await axios.patch(
// `${process.env.API}students/${
// ctx.from?.id
// }?Link=${encodeURIComponent(fileName)}`,
// {},
// {
// headers: {
// 'X-API-KEY':
// 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
// 'Content-Type': 'application/json',
// },
// }
// );
// await ctx.reply('Резюме успешно загружено.');
// await ctx.reply(
// 'Меню',
// Markup.keyboard([
// ['Моя анкета'],
// ['Вакансии'],
// ['Уведомления: включены'],
// ]).resize()
// );
// return ctx.scene.leave();
// } catch (uploadError) {
// console.error('Ошибка при загрузке резюме на API:', uploadError);
// await ctx.reply('Произошла ошибка при загрузке резюме.');
// return ctx.scene.leave();
// } finally {
// // Удаление временного файла после загрузки
// fs.unlinkSync(filePath);
// }
// });
// } catch (error) {
// console.error('Ошибка при загрузке файла:', error);
// await ctx.reply('Произошла ошибка при загрузке файла.');
// }
// } else {
// await ctx.reply('Отправьте файл в формате PDF');
// }
// }
// );
const resumeScene = new telegraf_1.Scenes.WizardScene('resumeScene', (ctx) => __awaiter(void 0, void 0, void 0, function* () {
yield ctx.reply('Отправьте своё резюме в формате Word (DOC/DOCX) или отправьте /cancel для отмены.');
return ctx.wizard.next();
}), (ctx) => __awaiter(void 0, void 0, void 0, function* () {
if (ctx.message &&
'text' in ctx.message &&
ctx.from &&
ctx.message.text === '/cancel') {
yield ctx.reply('Отправка резюме отменена.');
yield ctx.reply('Меню', telegraf_1.Markup.keyboard([
['Моя анкета'],
['Вакансии'],
['Уведомления: включены'],
]).resize());
return ctx.scene.leave();
}
if (ctx.message && 'document' in ctx.message && ctx.from) {
const file = ctx.message.document;
const allowedMimeTypes = [
// 'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
];
if (!allowedMimeTypes.includes(file.mime_type || '')) {
yield ctx.reply('Пожалуйста, отправьте файл в формате DOC или DOCX.');
return;
}
try {
const fileLink = yield ctx.telegram.getFileLink(file.file_id);
const fileExtension = file.mime_type === 'application/pdf'
? 'pdf'
: file.mime_type === 'application/msword'
? 'doc'
: 'docx';
const filePath = path_1.default.join(__dirname, `${file.file_id}.${fileExtension}`);
// Загрузка файла на локальную машину
const response = yield axios_1.default.get(fileLink.href, {
responseType: 'stream',
});
response.data
.pipe(fs_1.default.createWriteStream(filePath))
.on('finish', () => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const form = new form_data_1.default();
form.append('file', fs_1.default.createReadStream(filePath));
try {
const uploadResponse = yield axios_1.default.post(`${process.env.API}services/resume/`, form, {
headers: Object.assign(Object.assign({}, form.getHeaders()), { 'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n' }),
timeout: 10000,
});
const fileName = uploadResponse.data.filename;
yield axios_1.default.patch(`${process.env.API}students/${(_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id}?Link=${encodeURIComponent(fileName)}`, {}, {
headers: {
'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
'Content-Type': 'application/json',
},
});
yield ctx.reply('Резюме успешно загружено.');
yield ctx.reply('Меню', telegraf_1.Markup.keyboard([
['Моя анкета'],
['Вакансии'],
['Уведомления: включены'],
]).resize());
return ctx.scene.leave();
}
catch (uploadError) {
console.error('Ошибка при загрузке резюме на API:', uploadError);
yield ctx.reply('Произошла ошибка при загрузке резюме.');
return ctx.scene.leave();
}
finally {
fs_1.default.unlinkSync(filePath);
}
}));
}
catch (error) {
console.error('Ошибка при загрузке файла:', error);
yield ctx.reply('Произошла ошибка при загрузке файла.');
}
}
else {
yield ctx.reply('Пожалуйста, отправьте файл в формате PDF, DOC или DOCX.');
}
}));
// app.post('/api/resume', async (req: Request, res: Response) => {
// try {
// const postData = req.body;
// // Проверяем наличие chatId в теле запроса
// if (!postData.StudentID) {
// throw new Error('Chat ID is missing in the request body');
// }
// const [userInstance, created] = await Resume.findOrCreate({
// where: { id: postData.StudentID },
// defaults: {
// id: postData.StudentID || 0,
// name: postData.Name,
// group: postData.Group,
// type: postData.Type,
// skills: '',
// softskills: postData.Soft_skills,
// email: postData.Email,
// },
// });
// await UserBase.update(
// { resume_id: postData.StudentID },
// { where: { id: postData.StudentID } }
// );
// if (userInstance instanceof Resume) {
// // userInstance теперь имеет тип User
// if (created) {
// console.log('New user created:', userInstance);
// } else {
// console.log('User already exists:', userInstance);
// }
// console.log('Привет! Вы успешно зарегистрированы.');
// } else {
// console.error('Ошибка: userInstance не является экземпляром User.');
// }
// console.log('Received data:', postData);
// res.status(200).json({ message: 'Data received successfully' });
// } catch (error) {
// console.error(error);
// res.status(500).json({ error: 'Internal Server Error' });
// }
// });
app.post('/webhook', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
try {
const postData = req.body;
const entityType = postData.data.entity_type;
const updatedMatches = postData.data.updated_matches;
console.log(postData);
if (entityType === 'job') {
for (const match of updatedMatches) {
const chatId = match.student_id;
const jobId = match.entity_id; // Получаем ID вакансии
const messageText = 'Ваше совпадение с вакансией было обновлено. Вот детали вакансии:';
if (!chatId) {
throw new Error('Неправильный Chat ID (student_id) в теле запроса.');
}
// Выполняем GET-запрос для получения данных о вакансии
const response = yield axios_1.default.get(`${process.env.API}jobs/${jobId}`, // Запрашиваем данные о вакансии по её ID
{
headers: {
'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
});
const vacancyData = response.data; // Данные о вакансии
console.log(vacancyData);
// Отправляем вакансию пользователю
yield sendVacancyToUser(chatId, vacancyData);
console.log(`Сообщение с вакансией отправлено пользователю с ID ${chatId}`);
}
}
return res.status(200).json({ status: 'success' });
}
catch (e) {
console.error('Error:', e);
res.status(500).json({ error: 'Произошла ошибка при отправке сообщения' });
}
}));
// Функция для отправки вакансии пользователю
function sendVacancyToUser(chatId, data) {
return __awaiter(this, void 0, void 0, function* () {
yield bot.telegram.sendMessage(chatId, `<b>${data.Job_name}</b>\n\n` +
`<b>Компания:</b> ${data.Company_name}\n` +
`<b>Заработная плата:</b> ${data.Salary} руб/мес\n` +
`<b>Контактные данные:</b> ${data.Email}\n\n` +
`<b>Требования к кандидату:</b>\n` +
` - ${data.Year} курс\n` +
` - Опыт работы по специальности: ${data.Qualification}\n` +
` - Soft skills: ${data.Soft_skills}\n` +
`<b>Обязанности:</b>\n` +
`${data.Responsibilities}`, {
parse_mode: 'HTML', // Используем HTML для форматирования текста
reply_markup: {
inline_keyboard: [
[
{
text: 'Сохранить вакансию',
callback_data: `savevacancy+${data.JobID}`,
},
],
],
},
});
});
}
// Обработчик для получения файла резюме
const stage = new telegraf_1.Scenes.Stage([resumeScene]);
bot.use((0, telegraf_1.session)());
bot.use(stage.middleware());
bot.use(menuScenes_1.menuSceneMiddleware);
(0, events_1.events)(bot);
(0, accept_1.accept)(bot);
bot.action('uploadresume', (ctx) => __awaiter(void 0, void 0, void 0, function* () {
ctx.scene.enter('resumeScene');
ctx.answerCbQuery();
}));
(0, savevacansy_1.saveVacansy)(bot);
bot
.launch({
webhook: {
domain: process.env.DOMAIN || '',
port: parseInt(process.env.HOOKPORT || ''),
},
})
.then(() => console.log('Webhook bot listening on port', parseInt(process.env.HOOKPORT || '')));
// bot.launch().then(() => {
// console.log('Бот запущен');
// });
app.listen(process.env.PORT, () => {
console.log(`Server is running at http://localhost:${process.env.PORT}`);
});

24
mtucijobsbot/dist/languages/en.json vendored Normal file
View File

@@ -0,0 +1,24 @@
{
"greeting": {
"photo": "",
"file": "",
"text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты",
"buttons": [
[
{ "text": "Понял", "callback": "accept" },
{ "text": "Понял, не запрашивать данные", "callback": "accept" }
]
]
},
"greeting2": {
"photo": "",
"file": "",
"text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты",
"buttons": [
[
{ "text": "Понял", "callback": "accept2" },
{ "text": "Понял, не запрашивать данные", "callback": "accept2" }
]
]
}
}

24
mtucijobsbot/dist/languages/ru.json vendored Normal file
View File

@@ -0,0 +1,24 @@
{
"greeting": {
"photo": "",
"file": "",
"text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты",
"buttons": [
[
{ "text": "Понял", "callback": "accept" },
{ "text": "Понял, не запрашивать данные", "callback": "accept" }
]
]
},
"greeting2": {
"photo": "",
"file": "",
"text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты",
"buttons": [
[
{ "text": "Понял", "callback": "accept2" },
{ "text": "Понял, не запрашивать данные", "callback": "accept2" }
]
]
}
}

11
mtucijobsbot/dist/models/User.js vendored Normal file
View File

@@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserRole = void 0;
// Перечисление ролей пользователя
var UserRole;
(function (UserRole) {
UserRole["OWNER"] = "owner";
UserRole["ADMIN"] = "admin";
UserRole["MODERATOR"] = "moderator";
UserRole["CLIENT"] = "client";
})(UserRole || (exports.UserRole = UserRole = {}));

View File

@@ -0,0 +1,50 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MenuController = void 0;
const db_1 = require("../db/db");
class MenuController {
static showLanguageMenu(ctx) {
return __awaiter(this, void 0, void 0, function* () {
// await MessagesService.sendMessage(ctx, 'greeting');
});
}
static setLanguage(ctx) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
try {
if (!ctx.callbackQuery) {
throw new Error('CallbackQuery is not defined.');
}
const languageCode = ctx.callbackQuery && 'data' in ctx.callbackQuery
? ctx.callbackQuery.data.split('_')[1]
: undefined;
if (languageCode) {
// Обновляем язык пользователя в базе данных
yield db_1.UserBase.update({ language: languageCode }, { where: { id: (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id } });
// Обновляем язык в объекте контекста
if (ctx.from) {
ctx.from.language_code = languageCode;
}
}
else {
// Если data отсутствует в callbackQuery, обработка не может быть выполнена
throw new Error('Missing data property in callbackQuery.');
}
}
catch (error) {
console.error('Error during setLanguage:', error);
console.log('Произошла ошибка при обновлении языка. Пожалуйста, попробуйте снова позже.');
}
});
}
}
exports.MenuController = MenuController;

38
mtucijobsbot/dist/modules/menuScenes.js vendored Normal file
View File

@@ -0,0 +1,38 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.menuSceneMiddleware = void 0;
const db_1 = require("../db/db");
const messagesService_1 = require("../services/messagesService");
// Middleware для обработки callback'ов
const callbackQueryMiddleware = (ctx, next) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
try {
const languageCode = ctx.callbackQuery && 'data' in ctx.callbackQuery
? ctx.callbackQuery.data.split('_')[1]
: undefined;
// Обновляем язык пользователя в базе данных
if (languageCode && ((_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id)) {
yield db_1.UserBase.update({ language: languageCode }, { where: { id: ctx.from.id } });
// Отправляем сообщение об успешном обновлении языка
console.log(`Язык обновлен: ${languageCode}`);
yield ctx.answerCbQuery();
yield messagesService_1.MessagesService.sendMessage(ctx, 'greeting');
}
}
catch (error) {
console.error('Error during callback handling:', error);
console.log("Произошла ошибка при обработке callback'а. Пожалуйста, попробуйте снова позже.");
}
// Вызываем следующий middleware
return next();
});
exports.menuSceneMiddleware = callbackQueryMiddleware;

View File

@@ -0,0 +1,112 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.accept = void 0;
const axios_1 = __importDefault(require("axios"));
require("dotenv/config");
// const API_BASE_URL = 'https://mtucitech.ru/api/external/mtuci_jobs';
// const BEARER_TOKEN = 'zKbgaXQv{pvGtQm~U9$urtD#QsiHc@Ie';
function accept(bot) {
bot.action('accept', (ctx) => __awaiter(this, void 0, void 0, function* () {
var _a;
try {
const response = yield axios_1.default.get(`${process.env.MTUCI_TECH}/check?telegram_id=${ctx.from.id}`, {
headers: {
Authorization: `Bearer ${process.env.BEARER_TOKEN}`,
},
});
const data = response.data;
// Логика обработки ответа
switch (data.status) {
case 338:
ctx.answerCbQuery('Ошибка на стороне сервера. Пожалуйста, сообщите нам.');
break;
case 339:
ctx.answerCbQuery('Пользователь не привязан к проекту.');
break;
case 340:
ctx.reply('Можете заполнить своё резюме', {
reply_markup: {
inline_keyboard: [
[
{
text: 'Резюме',
web_app: { url: process.env.WEB_APP || '' },
},
],
],
resize_keyboard: true,
one_time_keyboard: true,
},
});
ctx.answerCbQuery('Пользователь найден, но не привязан LMS.');
break;
case 341:
ctx.reply('Можете заполнить своё резюме', {
reply_markup: {
inline_keyboard: [
[
{
text: 'Резюме',
web_app: { url: process.env.WEB_APP || '' },
},
],
],
resize_keyboard: true,
one_time_keyboard: true,
},
});
ctx.answerCbQuery('Пользователь найден, LMS привязан. Можно запросить дополнительные данные.');
default:
ctx.answerCbQuery('Неизвестный статус.');
break;
}
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
// Обработка ошибок Axios
if (error.response && error.response.status === 409) {
ctx.answerCbQuery('Вакансия уже добавлена в избранное');
}
else {
ctx.answerCbQuery('Произошла ошибка');
console.error(((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) || error.message);
}
}
else {
// Обработка других типов ошибок
ctx.answerCbQuery('Произошла неизвестная ошибка');
console.error(error);
}
}
}));
bot.action('accept2', (ctx) => __awaiter(this, void 0, void 0, function* () {
ctx.reply('Можете заполнить своё резюме', {
reply_markup: {
inline_keyboard: [
[
{
text: 'Резюме',
web_app: { url: process.env.WEB_APP || '' },
},
],
],
resize_keyboard: true,
one_time_keyboard: true,
},
});
ctx.answerCbQuery();
}));
}
exports.accept = accept;

View File

@@ -0,0 +1,292 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.events = void 0;
const telegraf_1 = require("telegraf");
const db_1 = require("../../db/db");
const axios_1 = __importDefault(require("axios"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
let vacancies = [];
function events(bot) {
bot.action('skip', (ctx) => __awaiter(this, void 0, void 0, function* () {
ctx.reply('Меню', telegraf_1.Markup.keyboard([
['Моя анкета'],
['Вакансии'],
// ['Уведомления: включены'],
]).resize());
ctx.answerCbQuery();
}));
bot.hears('Вернуться в меню', (ctx) => __awaiter(this, void 0, void 0, function* () {
ctx.reply('Меню', telegraf_1.Markup.keyboard([
['Моя анкета'],
['Вакансии'],
// ['Уведомления: включены'],
]).resize());
}));
bot.hears('Моя анкета', (ctx) => __awaiter(this, void 0, void 0, function* () {
try {
// Выполнение первого GET-запроса для получения данных пользователя
const response = yield axios_1.default.get(`${process.env.API}students/${ctx.from.id}`, {
headers: {
'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
});
const data = response.data;
const resumeFile = yield db_1.Resume.findOne({ where: { id: ctx.from.id } });
// Проверяем, существует ли резюме
if (resumeFile && resumeFile.resumefile.length > 1) {
// Выполнение второго GET-запроса для получения файла с резюме
const resumeResponse = yield (0, axios_1.default)({
method: 'GET',
url: `${process.env.API}services/resume/${resumeFile.resumefile}`,
headers: {
'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
responseType: 'stream',
timeout: 10000,
});
// Сохраняем файл временно на диск
const resumePath = path_1.default.join(__dirname, 'resume.pdf');
const writer = fs_1.default.createWriteStream(resumePath);
resumeResponse.data.pipe(writer);
writer.on('finish', () => __awaiter(this, void 0, void 0, function* () {
// Отправка файла и сообщения с данными из API
yield ctx.replyWithDocument({ source: resumePath });
yield ctx.replyWithHTML(`<b>Имя:</b> ${data.Name}\n<b>Группа:</b> ${data.Group}\n<b>Тип:</b> ${data.Type}\n<b>Готов на занятость:</b> ${data.Time.join()}\n<b>О себе:</b> ${data.Soft_skills}\n<b>Почта:</b> ${data.Email}\n`, {
reply_markup: {
inline_keyboard: [
[
{
text: 'Редактировать анкету',
web_app: { url: process.env.WEB_APP || '' },
},
],
[
{
text: 'Обновить резюме',
callback_data: 'update_resume',
},
],
],
},
});
// Удаляем временный файл
fs_1.default.unlinkSync(resumePath);
}));
writer.on('error', err => {
console.error('Ошибка при записи файла резюме:', err);
ctx.reply('Произошла ошибка при получении файла резюме.');
});
}
else {
// Отправка только сообщения с данными из API, если резюме не существует
yield ctx.replyWithHTML(`<b>Имя:</b> ${data.Name}\n<b>Группа:</b> ${data.Group}\n<b>Тип:</b> ${data.Type}\n<b>Готов на занятость:</b> ${data.Time.join()}\n<b>О себе:</b> ${data.Soft_skills}\n<b>Почта:</b> ${data.Email}\n`, {
reply_markup: {
inline_keyboard: [
[
{
text: 'Редактировать',
web_app: { url: process.env.WEB_APP || '' },
},
],
[
{
text: 'Загрузить резюме',
callback_data: 'update_resume',
},
],
],
},
});
}
}
catch (error) {
console.error('Ошибка при выполнении запроса к API:', error);
ctx.reply('Произошла ошибка при получении данных из API.');
}
}));
bot.hears('Вакансии', (ctx) => __awaiter(this, void 0, void 0, function* () {
try {
yield ctx.reply('Выберите тип вакансии:', telegraf_1.Markup.keyboard([
['Актуальные вакансии'],
['Сохраненные вакансии'],
['Поиск'],
['Вернуться в меню'],
]).resize());
}
catch (error) {
console.error('Ошибка при выполнении запроса к API:', error);
ctx.reply('Произошла ошибка при получении данных из API.');
}
}));
bot.hears('Поиск', (ctx) => __awaiter(this, void 0, void 0, function* () {
try {
yield ctx.reply('Поиск вакансий:', telegraf_1.Markup.keyboard([
['Актуальные вакансии'],
['Сохраненные вакансии'],
['Вернуться в меню'],
]).resize());
ctx.reply('Страница с поиском вакансий', {
reply_markup: {
inline_keyboard: [
[
{
text: 'Перейти',
web_app: { url: process.env.WEB_APP_SEARCH || '' },
},
],
],
resize_keyboard: true,
one_time_keyboard: true,
},
});
}
catch (error) {
console.error('Ошибка при выполнении запроса к API:', error);
ctx.reply('Произошла ошибка при получении данных из API.');
}
}));
bot.hears('Актуальные вакансии', (ctx) => __awaiter(this, void 0, void 0, function* () {
try {
yield db_1.UserBase.update({ vacansyindex: 0 }, {
where: {
id: ctx.from.id,
},
});
// Выполнение первого GET-запроса для получения данных пользователя
const response = yield axios_1.default.get(`${process.env.API}students/matches/${ctx.from.id}`, {
headers: {
'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
});
vacancies = response.data;
console.log(vacancies);
if (vacancies.length === 0) {
ctx.reply('На данный момент нет актуальных вакансий.');
return;
}
let user = yield db_1.UserBase.findOne({ where: { id: ctx.from.id } });
if (user) {
let currentIndex = user === null || user === void 0 ? void 0 : user.vacansyindex;
yield sendVacancy(ctx, currentIndex, true);
}
}
catch (error) {
console.error('Ошибка при выполнении запроса к API:', error);
ctx.reply('Произошла ошибка при получении данных из API.');
}
}));
bot.hears('Сохраненные вакансии', (ctx) => __awaiter(this, void 0, void 0, function* () {
try {
yield db_1.UserBase.update({ vacansyindex: 0 }, {
where: {
id: ctx.from.id,
},
});
// Выполнение первого GET-запроса для получения данных пользователя
const response = yield axios_1.default.get(`${process.env.API}students/favourites/${ctx.from.id}`, {
headers: {
'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
});
vacancies = response.data;
console.log(vacancies);
if (vacancies.length === 0) {
ctx.reply('На данный момент нет сохранённых вакансий.');
return;
}
let user = yield db_1.UserBase.findOne({ where: { id: ctx.from.id } });
if (user) {
let currentIndex = user === null || user === void 0 ? void 0 : user.vacansyindex;
yield sendVacancy(ctx, currentIndex, false);
}
}
catch (error) {
console.error('Ошибка при выполнении запроса к API:', error);
ctx.reply('На данный момент нет сохранённых вакансий.');
// ctx.reply('Произошла ошибка при получении данных из API.');
}
}));
bot.action('next', (ctx) => __awaiter(this, void 0, void 0, function* () {
let user = yield db_1.UserBase.findOne({ where: { id: ctx.from.id } });
if (user) {
let currentIndex = (user === null || user === void 0 ? void 0 : user.vacansyindex) + 1;
if (currentIndex <= vacancies.length - 1) {
yield db_1.UserBase.update({ vacansyindex: currentIndex }, { where: { id: ctx.from.id } });
yield sendVacancy(ctx, currentIndex, true);
ctx.answerCbQuery();
}
else {
ctx.answerCbQuery('Это последняя вакансия.');
}
}
}));
bot.action('update_resume', (ctx) => __awaiter(this, void 0, void 0, function* () {
yield ctx.scene.enter('resumeScene');
yield ctx.answerCbQuery();
}));
bot.action('prev', (ctx) => __awaiter(this, void 0, void 0, function* () {
let user = yield db_1.UserBase.findOne({ where: { id: ctx.from.id } });
if (user) {
let currentIndex = (user === null || user === void 0 ? void 0 : user.vacansyindex) - 1;
if (currentIndex >= 0) {
yield db_1.UserBase.update({ vacansyindex: currentIndex }, { where: { id: ctx.from.id } });
yield sendVacancy(ctx, currentIndex, true);
ctx.answerCbQuery();
}
else {
ctx.answerCbQuery('Это первая вакансия.');
}
}
}));
function sendVacancy(ctx, index, showSaveButton) {
return __awaiter(this, void 0, void 0, function* () {
const data = vacancies[index];
// Формируем кнопки навигации
let inlineKeyboard = [
[
{ text: 'Назад', callback_data: 'prev' },
{ text: `${index + 1}/${vacancies.length}`, callback_data: 'new' },
{ text: 'Далее', callback_data: 'next' },
],
];
// Добавляем кнопку "Сохранить вакансию" только если showSaveButton = true
if (showSaveButton) {
inlineKeyboard.push([
{
text: 'Сохранить вакансию',
callback_data: `savevacancy+${data.JobID}`,
},
]);
}
// Отправляем сообщение с вакансиями
yield ctx.replyWithHTML(`<b>${data.Job_name}</b>\n\n` +
`<b>Компания:</b> ${data.Company_name}\n` +
`<b>Заработная плата:</b> ${data.Salary} руб/мес\n` +
`<b>Контактные данные:</b> ${data.Email}\n\n` +
`<b>Требования к кандидату:</b>\n` +
` - ${data.Year} курс\n` +
` - Опыт работы по специальности: ${data.Qualification}\n` +
` - Soft skills: ${data.Soft_skills}\n` +
`<b>Обязанности:</b>\n` +
`${data.Responsibilities}`, {
reply_markup: {
inline_keyboard: inlineKeyboard,
},
});
});
}
}
exports.events = events;

View File

@@ -0,0 +1,39 @@
"use strict";
// import { Telegraf, Context, Scenes } from 'telegraf';
// import { MessagesService } from '../../services/messagesService';
// import fs from 'fs';
// import fetch from 'node-fetch';
// import { MyWizardContext } from '../..';
// import { updateUrls } from '../../db/db';
// export function invest(bot: Telegraf<Scenes.WizardContext>) {
// bot.action('invest', async ctx => {
// await MessagesService.sendMessage(ctx, 'invest');
// await ctx.answerCbQuery();
// });
// bot.action('investscreen', async ctx => {
// await ctx.scene.enter('investHandler');
// await ctx.answerCbQuery();
// });
// }
// export const investHandler: Scenes.WizardScene<MyWizardContext> =
// new Scenes.WizardScene(
// 'investHandler',
// async ctx => {
// await MessagesService.sendMessage(ctx, 'getpoint');
// return ctx.wizard.next();
// },
// async ctx => {
// if (ctx.message && 'text' in ctx.message && ctx.from) {
// const userUrls = ctx.message.text;
// await updateUrls(ctx.from.id, 'invest', userUrls, true);
// await MessagesService.sendMessage(ctx, 'thanks');
// await MessagesService.sendMessage(ctx, 'invest');
// return ctx.scene.leave();
// } else {
// console.error(
// 'Ошибка: отсутствует текстовое сообщение или свойство from в контексте.'
// );
// return ctx.scene.leave();
// }
// }
// );

View File

@@ -0,0 +1,55 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveVacansy = void 0;
const axios_1 = __importDefault(require("axios"));
require("dotenv/config");
function saveVacansy(bot) {
bot.on('callback_query', (ctx) => __awaiter(this, void 0, void 0, function* () {
var _a;
if (ctx.callbackQuery && 'data' in ctx.callbackQuery) {
const callbackData = ctx.callbackQuery.data;
if (callbackData.startsWith('savevacancy+')) {
const jobID = callbackData.split('+')[1];
try {
const data = yield axios_1.default.post(`${process.env.API}students/favourites/`, { StudentID: ctx.from.id, JobID: jobID }, {
headers: {
'X-API-KEY': 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
});
console.log(data);
ctx.answerCbQuery('Вакансия была сохранена');
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
// Обработка ошибок Axios
if (error.response && error.response.status === 409) {
ctx.answerCbQuery('Вакансия уже добавлена в избранное');
}
else {
ctx.answerCbQuery('Произошла ошибка при сохранении вакансии');
console.error(((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) || error.message);
}
}
else {
// Обработка других типов ошибок
ctx.answerCbQuery('Произошла неизвестная ошибка');
console.error(error);
}
}
}
}
}));
}
exports.saveVacansy = saveVacansy;

View File

@@ -0,0 +1,277 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MessagesService = void 0;
// Импортируем необходимые модули
const fs = __importStar(require("fs"));
// import { CustomContext } from '../types/telegram';
const path_1 = __importDefault(require("path"));
const db_1 = require("../db/db");
const telegraf_1 = require("telegraf");
class MessagesService {
static loadMessages(lang) {
if (!this.messages[lang]) {
const filePath = path_1.default.join(__dirname, `../languages/${lang}.json`);
const data = fs.readFileSync(filePath, 'utf-8');
this.messages[lang] = JSON.parse(data);
}
return this.messages[lang];
}
static backMessage(ctx) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
try {
const user = (yield db_1.UserBase.findOne({
where: {
id: (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id,
},
}));
if (user !== null && ctx.chat) {
yield ctx.telegram.copyMessage((_b = ctx.chat) === null || _b === void 0 ? void 0 : _b.id, (_c = ctx.chat) === null || _c === void 0 ? void 0 : _c.id, user.message_id - 1);
}
else {
console.log('error');
}
}
catch (error) {
console.log(error);
}
});
}
static hiddenMessage(ctx, key) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f;
try {
const user = (yield db_1.UserBase.findOne({
where: {
id: (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id,
},
}));
if (user && user.message_id && ((_b = ctx.callbackQuery) === null || _b === void 0 ? void 0 : _b.message)) {
yield ctx.telegram.deleteMessage(((_c = ctx.chat) === null || _c === void 0 ? void 0 : _c.id) || 0, (_e = (_d = ctx.callbackQuery) === null || _d === void 0 ? void 0 : _d.message) === null || _e === void 0 ? void 0 : _e.message_id);
console.log('Hidden message deleted successfully.');
yield db_1.UserBase.update({ last_obj: key }, // Новое значение
{
where: {
id: (_f = ctx.from) === null || _f === void 0 ? void 0 : _f.id, // Условие для выбора записей
},
});
}
else {
console.log('User or message not found.');
}
}
catch (error) {
console.error('Error deleting last message:', error);
ctx.reply('An error occurred while deleting the last message. Please try again later.');
}
});
}
static deleteLastMessage(ctx) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f, _g, _h;
try {
const user = (yield db_1.UserBase.findOne({
where: {
id: (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id,
},
}));
if (user && user.message_id && ((_b = ctx.callbackQuery) === null || _b === void 0 ? void 0 : _b.message)) {
yield ctx.telegram.deleteMessage(((_c = ctx.chat) === null || _c === void 0 ? void 0 : _c.id) || 0, (_e = (_d = ctx.callbackQuery) === null || _d === void 0 ? void 0 : _d.message) === null || _e === void 0 ? void 0 : _e.message_id);
console.log('Last message deleted successfully.');
yield db_1.UserBase.update({ message_id: (_g = (_f = ctx.callbackQuery) === null || _f === void 0 ? void 0 : _f.message) === null || _g === void 0 ? void 0 : _g.message_id }, // Новое значение
{
where: {
id: (_h = ctx.from) === null || _h === void 0 ? void 0 : _h.id, // Условие для выбора записей
},
});
}
else {
console.log('User or message not found.');
}
}
catch (error) {
console.error('Error deleting last message:', error);
ctx.reply('An error occurred while deleting the last message. Please try again later.');
}
});
}
static sendMessage(ctx, key) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
try {
const user = (yield db_1.UserBase.findOne({
where: {
id: (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id,
},
}));
if (user !== null) {
const lang = user.language;
const messages = this.loadMessages(lang);
const message = messages[key];
if (message) {
// Определяем, есть ли у сообщения фотография
const hasPhoto = message.photo !== '';
const hasFile = message.file !== '';
if (hasPhoto &&
message.buttons &&
message.buttons.length > 0 &&
message.photo) {
const photoPath = path_1.default.join(__dirname, message.photo);
if (message.buttons && message.buttons.length > 0) {
const rows = message.buttons;
if (Array.isArray(rows) &&
rows.every(row => Array.isArray(row))) {
const sentMessage = yield ctx.replyWithPhoto({ source: fs.createReadStream(photoPath) || '' }, Object.assign({ protect_content: true, caption: message.text, parse_mode: 'HTML' }, telegraf_1.Markup.inlineKeyboard(rows.map(row => row.map(button => {
if (button.url) {
return telegraf_1.Markup.button.url(button.text, button.url);
}
else {
return telegraf_1.Markup.button.callback(button.text, button.callback);
}
})))));
if (sentMessage.message_id) {
yield db_1.UserBase.update({ message_id: sentMessage.message_id }, {
where: {
id: (_b = ctx.from) === null || _b === void 0 ? void 0 : _b.id,
},
});
}
}
else {
console.error('rows должен быть массивом массивов');
}
}
}
else if (hasPhoto && message.photo) {
const photoPath = path_1.default.join(__dirname, message.photo);
const sentMessage = yield ctx.replyWithPhoto({ source: fs.createReadStream(photoPath) || '' }, {
protect_content: true,
caption: message.text,
parse_mode: 'HTML',
});
if (sentMessage.message_id) {
yield db_1.UserBase.update({ message_id: sentMessage.message_id }, {
where: {
id: (_c = ctx.from) === null || _c === void 0 ? void 0 : _c.id,
},
});
}
}
else if (hasFile && message.file) {
const pdfFilePath = path_1.default.join(__dirname, message.file);
const fileName = path_1.default.basename(pdfFilePath);
// Отправляем файл
const sentMessage = yield ctx.replyWithDocument({
source: fs.createReadStream(pdfFilePath) || '',
filename: fileName,
}, {
protect_content: true,
caption: message.text,
parse_mode: 'HTML',
});
if (sentMessage.message_id) {
yield db_1.UserBase.update({ message_id: sentMessage.message_id }, {
where: {
id: (_d = ctx.from) === null || _d === void 0 ? void 0 : _d.id,
},
});
}
}
else {
// Отправляем текст и кнопки, если они есть
if (message.buttons && message.buttons.length > 0) {
const rows = message.buttons;
if (Array.isArray(rows) &&
rows.every(row => Array.isArray(row))) {
const sentMessage = yield ctx.reply(message.text, Object.assign({ protect_content: true, parse_mode: 'HTML' }, telegraf_1.Markup.inlineKeyboard(rows.map(row => row.map(button => {
if (button.url) {
return telegraf_1.Markup.button.url(button.text, button.url);
}
else {
return telegraf_1.Markup.button.callback(button.text, button.callback);
}
})))));
if (sentMessage.message_id) {
yield db_1.UserBase.update({ message_id: sentMessage.message_id }, {
where: {
id: (_e = ctx.from) === null || _e === void 0 ? void 0 : _e.id,
},
});
}
}
else {
console.error('rows должен быть массивом массивов');
}
}
else {
// Отправляем только текст, если нет кнопок
const sentMessage = yield ctx.replyWithHTML(message.text, {
protect_content: true,
});
yield db_1.UserBase.update({ message_id: (_g = (_f = ctx.callbackQuery) === null || _f === void 0 ? void 0 : _f.message) === null || _g === void 0 ? void 0 : _g.message_id }, {
where: {
id: (_h = ctx.from) === null || _h === void 0 ? void 0 : _h.id,
},
});
if (sentMessage.message_id) {
yield db_1.UserBase.update({ message_id: sentMessage.message_id }, {
where: {
id: (_j = ctx.from) === null || _j === void 0 ? void 0 : _j.id,
},
});
}
}
}
}
else {
console.error(`Key not found: ${key}`);
}
}
else {
console.error('User not found.');
}
}
catch (error) {
console.error('Error fetching user language:', error);
console.log('An error occurred. Please try again later.');
}
});
}
}
exports.MessagesService = MessagesService;
MessagesService.messages = {};

View File

@@ -0,0 +1,62 @@
version: '3.9'
x-common-logging: &common-logging
logging:
# limit logs retained on host to 25MB
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
x-common-network: &common-network
networks:
- mtuci-jobs
services:
bot-mtuci-jobs:
image: image-mtuci-jobs
build:
context: .
dockerfile: Dockerfile
container_name: bot-mtuci-jobs
restart: always
depends_on:
postgres-mtuci-jobs:
condition: service_healthy
ports:
- '127.0.0.1:3005:3005'
- '127.0.0.1:3006:3006'
environment:
DATABASE_URL: 'postgres://postgres:password@postgres-mtuci-jobs:5432/mtuci-jobs'
DB_NAME: mtucijobs
DB_USER: postgres
DB_PASSWORD: password
DB_HOST: postgres-mtuci-jobs
BOT_TOKEN: '7306496213:AAGuvha0ytpF8keCZpes8BdP1AnfP7yEiSE'
HOOKPORT: 3005
PORT: 3006
DOMAIN: ''
WEB_APP: ''
<<: [*common-network, *common-logging]
postgres-mtuci-jobs:
image: postgres:16.1-alpine3.19
container_name: bot-postgres-mtuci-jobs
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: mtucijobs
PGDATA: /data/postgres
POSTGRES_HOST_AUTH_METHOD: trust
healthcheck:
test: pg_isready -U postgres
interval: 5s
timeout: 5s
retries: 5
volumes:
- /data/mtuci-jobs/data/postgres:/data/postgres
<<: [*common-network, *common-logging]
networks:
mtuci-jobs:
driver: bridge

3007
mtucijobsbot/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
mtucijobsbot/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "mtucijobs",
"version": "1.0.0",
"description": "This bot for afree",
"main": "index.js",
"scripts": {
"build": "tsc && copyfiles -u 1 src/**/*.json src/assets/**/* dist",
"start": "node ./dist/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "student-programmer",
"license": "ISC",
"dependencies": {
"axios": "^1.7.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"form-data": "^4.0.0",
"fs": "^0.0.1-security",
"path": "^0.12.7",
"pg": "^8.12.0",
"sequelize": "^6.37.3",
"telegraf": "^4.16.3",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
},
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/cors": "^2.8.17",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.21",
"@types/form-data": "^2.5.0",
"@types/node": "^20.14.2",
"copyfiles": "^2.4.1",
"eslint": "^9.4.0"
}
}

14
mtucijobsbot/run.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
docker stop mtuci-jobs || echo "1"
docker rm mtuci-jobs || echo "2"
docker run -d \
--restart always \
-h mtuci-jobs \
--name mtuci-jobs \
-e BOT_TOKEN="7306496213:AAGuvha0ytpF8keCZpes8BdP1AnfP7yEiSE" \
--log-opt mode=non-blocking \
--log-opt max-size=10m \
--log-opt max-file=3 \
mtuci-jobs-image:latest

View File

@@ -0,0 +1,34 @@
import express from 'express';
import 'dotenv/config';
const router = express.Router();
router.post('/api/resume', async (req, res) => {
try {
const postData = req.body;
// Проверяем наличие chatId в теле запроса
if (!postData.chatId) {
throw new Error('Chat ID is missing in the request body');
}
// Ваша логика обработки данных
console.log('Received data:', postData.id);
// Отправка сообщения в боте
// await req.bot.telegram.sendMessage(
// postData.chatId,
// `Received data: ${postData.id}`
// );
// MessagesService.sendMessage(postData.id, )
res.status(200).json({ message: 'Data received successfully' });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
export default router;

Binary file not shown.

View File

@@ -0,0 +1,90 @@
import { Context } from 'telegraf';
import { MenuController } from '../modules/menuController';
import { Resume, UserBase, sequelize } from '../db/db';
import { UserRole } from '../models/User';
import axios from 'axios';
export async function startCommand(ctx: Context): Promise<void> {
function isBotOwner(userId: number | undefined): boolean {
const ownerUserId = 1053404914; // Замените этот ID на фактический ID владельца
return userId === ownerUserId;
}
try {
await sequelize.authenticate(); // Проверка подключения к базе данных
await UserBase.sync(); // Создание таблицы, если её нет
console.log('User table has been created or already exists.');
// Проверка, существует ли пользователь в базе данных
const [userInstance, created] = await UserBase.findOrCreate({
where: { id: ctx.from?.id },
defaults: {
id: ctx.from?.id || 0,
username: ctx.from?.username || '',
vacansyindex: 0,
role: isBotOwner(ctx.from?.id) ? UserRole.OWNER : UserRole.CLIENT,
language: ctx.from?.language_code || 'ru',
message_id: ctx.callbackQuery?.message?.message_id || 0,
last_obj: '',
chat_id: ctx.chat?.id,
resume_id: null,
},
});
// Выполняем запрос на удаление резюме, если оно существует
try {
const response = await axios.delete(
`${process.env.API}students/${ctx.from?.id}`,
{
headers: {
'X-API-KEY':
'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
}
);
if (response.status === 204) {
console.log('Резюме успешно удалено на сервере.');
await Resume.destroy({ where: { id: ctx.from?.id } }); // Удаляем запись из базы данных
await ctx.reply('Ваше резюме было удалено.');
} else {
console.warn(
'Не удалось удалить резюме, сервер вернул статус:',
response.status
);
await ctx.reply('Не удалось удалить ваше резюме. Попробуйте позже.');
}
} catch (error) {
console.error('Ошибка при удалении резюме:', error);
await ctx.reply(
'Произошла ошибка при удалении резюме. Пожалуйста, попробуйте позже.'
);
}
// Приветственное сообщение
const greetingMessage = {
text:
'Вас приветствует бот MTUCI jobs! \n\n' +
'Для подтверждения того, что вы студент МТУСИ, вам потребуется привязать вашу учетную запись LMS к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты.',
buttons: [[{ text: 'Понял', callback: 'accept2' }]],
};
// Отправка приветственного сообщения с кнопками
await ctx.reply(greetingMessage.text, {
reply_markup: {
inline_keyboard: greetingMessage.buttons.map(row =>
row.map(button => ({
text: button.text,
callback_data: button.callback,
}))
),
},
parse_mode: 'HTML',
});
console.log('Бот запущен');
} catch (e) {
console.error('Произошла ошибка при запуске бота', e);
ctx.reply('Произошла ошибка при запуске. Пожалуйста, попробуйте позже.');
}
}

176
mtucijobsbot/src/db/db.ts Normal file
View File

@@ -0,0 +1,176 @@
import 'dotenv/config';
import { DataTypes, Sequelize, Model } from 'sequelize';
const sequelize = new Sequelize(
process.env.DB_NAME || 'custdev',
process.env.DB_USER || 'postgres',
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST,
dialect: 'postgres',
}
);
// Определение интерфейса для модели
interface UserAttributes {
id: number;
username: string;
vacansyindex: number;
role: string;
language: string;
message_id: number | null;
last_obj: string | null;
chat_id: number | null;
resume_id: number | null; // добавляем внешний ключ
}
interface UserCreationAttributes extends UserAttributes {}
class UserBase
extends Model<UserAttributes, UserCreationAttributes>
implements UserAttributes
{
public id!: number;
public username!: string;
public vacansyindex!: number;
public role!: string;
public language!: string;
public message_id!: number | null;
public last_obj!: string | null;
public chat_id!: number | null;
public resume_id!: number | null; // добавляем внешний ключ
}
UserBase.init(
{
id: {
type: DataTypes.BIGINT,
primaryKey: true,
},
username: {
type: DataTypes.STRING,
allowNull: false,
},
vacansyindex: {
type: DataTypes.INTEGER,
allowNull: false,
},
role: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'client',
},
language: {
type: DataTypes.STRING,
allowNull: false,
},
message_id: {
type: DataTypes.INTEGER,
allowNull: true,
},
last_obj: {
type: DataTypes.STRING,
allowNull: true,
},
chat_id: {
type: DataTypes.BIGINT,
allowNull: true,
},
resume_id: {
type: DataTypes.BIGINT,
allowNull: true,
references: {
model: 'Resumes', // имя таблицы резюме
key: 'id',
},
},
},
{
sequelize,
modelName: 'User',
}
);
interface ResumeAttributes {
id: number;
name: string;
group: string;
type: string;
skills: string;
softskills: string;
email: string;
resumefile: string;
}
interface ResumeCreationAttributes extends ResumeAttributes {}
class Resume
extends Model<ResumeAttributes, ResumeCreationAttributes>
implements ResumeAttributes
{
public id!: number;
public name!: string;
public group!: string;
public type!: string;
public skills!: string;
public softskills!: string;
public email!: string;
public resumefile!: string;
}
Resume.init(
{
id: {
type: DataTypes.BIGINT,
primaryKey: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
group: {
type: DataTypes.STRING,
allowNull: false,
},
type: {
type: DataTypes.STRING,
allowNull: false,
},
skills: {
type: DataTypes.STRING,
allowNull: false,
},
softskills: {
type: DataTypes.STRING,
allowNull: false,
},
email: {
type: DataTypes.STRING,
allowNull: false,
},
resumefile: {
type: DataTypes.STRING,
allowNull: false,
},
},
{
sequelize,
modelName: 'Resume',
}
);
UserBase.belongsTo(Resume, { foreignKey: 'resume_id', as: 'resume' });
Resume.hasOne(UserBase, { foreignKey: 'resume_id', as: 'user' });
export { sequelize, UserBase, Resume };
sequelize
.sync()
.then(() => {
console.log('Model has been synchronized successfully.');
})
.catch(error => {
console.error('Error synchronizing model:', error);
});

469
mtucijobsbot/src/index.ts Normal file
View File

@@ -0,0 +1,469 @@
import 'dotenv/config';
import { Markup, Scenes, Telegraf, session } from 'telegraf';
import { startCommand } from './commands/start';
import { menuSceneMiddleware } from './modules/menuScenes';
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import { events } from './modules/scenes/events';
import { Resume } from './db/db';
import FormData from 'form-data';
import fs from 'fs';
import path from 'path';
import axios from 'axios';
import { saveVacansy } from './modules/scenes/savevacansy';
import { accept } from './modules/scenes/accept';
export type MyWizardContext = Scenes.WizardContext<Scenes.WizardSessionData>;
const app = express();
declare global {
namespace Express {
interface Request {
bot: Telegraf<Scenes.WizardContext>;
}
}
}
const bot = new Telegraf<Scenes.WizardContext>(process.env.BOT_TOKEN as string);
app.use((req: Request, res: Response, next: NextFunction) => {
req.bot = bot;
next();
});
app.use(express.json());
app.use(cors());
bot.start(startCommand);
app.post('/api/resume', async (req: Request, res: Response) => {
try {
const postData = req.body;
const resumeTemplatePath = path.resolve(
__dirname,
'assets',
'шаблон МТУСИ.docx'
);
await bot.telegram.sendDocument(
postData.id,
{ source: resumeTemplatePath },
{ caption: 'Пример резюме' }
);
await bot.telegram.sendMessage(
postData.id,
`Ваша анкета заполнена! Отправьте своё резюме для завершения регистрации`,
{
reply_markup: {
inline_keyboard: [
[
{ text: 'Загрузить резюме', callback_data: 'uploadresume' },
// { text: 'Пропустить загрузку', callback_data: 'skip' },
],
],
},
}
);
console.log(`Ваш id ${postData.id}`);
return res.json({ status: 'success' });
} catch (e) {
console.error('Error:', e);
res.status(500).json({ error: 'An error occurred' });
throw e;
}
});
// const resumeScene: Scenes.WizardScene<MyWizardContext> = new Scenes.WizardScene(
// 'resumeScene',
// async ctx => {
// await ctx.reply(
// 'Отправьте своё резюме в PDF формате или отправьте /cancel для отмены.'
// );
// return ctx.wizard.next();
// },
// async ctx => {
// if (
// ctx.message &&
// 'text' in ctx.message &&
// ctx.from &&
// ctx.message.text == '/cancel'
// ) {
// await ctx.reply('Отправка резюме отменена.');
// await ctx.reply(
// 'Меню',
// Markup.keyboard([
// ['Моя анкета'],
// ['Вакансии'],
// ['Уведомления: включены'],
// ]).resize()
// );
// return ctx.scene.leave();
// }
// if (ctx.message && 'document' in ctx.message && ctx.from) {
// const file = ctx.message.document;
// if (file.mime_type !== 'application/pdf') {
// ctx.reply('Пожалуйста, отправьте файл в формате PDF.');
// return;
// }
// try {
// const fileLink = await ctx.telegram.getFileLink(file.file_id);
// const filePath = path.join(__dirname, `${file.file_id}.pdf`);
// // Загрузка файла на локальную машину
// const response = await axios.get(fileLink.href, {
// responseType: 'stream',
// });
// response.data
// .pipe(fs.createWriteStream(filePath))
// .on('finish', async () => {
// // Создание формы данных
// const form = new FormData();
// form.append('file', fs.createReadStream(filePath));
// try {
// // Установка времени ожидания (в миллисекундах)
// const uploadResponse = await axios.post(
// `${process.env.API}services/resume/`,
// form,
// {
// headers: {
// ...form.getHeaders(),
// 'X-API-KEY':
// 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
// },
// timeout: 10000, // Увеличьте время ожидания до 10 секунд
// }
// );
// const fileName = uploadResponse.data.filename;
// // Выполнение PUT-запроса для обновления поля Link у студента
// await axios.patch(
// `${process.env.API}students/${
// ctx.from?.id
// }?Link=${encodeURIComponent(fileName)}`,
// {},
// {
// headers: {
// 'X-API-KEY':
// 'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
// 'Content-Type': 'application/json',
// },
// }
// );
// await ctx.reply('Резюме успешно загружено.');
// await ctx.reply(
// 'Меню',
// Markup.keyboard([
// ['Моя анкета'],
// ['Вакансии'],
// ['Уведомления: включены'],
// ]).resize()
// );
// return ctx.scene.leave();
// } catch (uploadError) {
// console.error('Ошибка при загрузке резюме на API:', uploadError);
// await ctx.reply('Произошла ошибка при загрузке резюме.');
// return ctx.scene.leave();
// } finally {
// // Удаление временного файла после загрузки
// fs.unlinkSync(filePath);
// }
// });
// } catch (error) {
// console.error('Ошибка при загрузке файла:', error);
// await ctx.reply('Произошла ошибка при загрузке файла.');
// }
// } else {
// await ctx.reply('Отправьте файл в формате PDF');
// }
// }
// );
const resumeScene: Scenes.WizardScene<MyWizardContext> = new Scenes.WizardScene(
'resumeScene',
async ctx => {
await ctx.reply(
'Отправьте своё резюме в формате Word (DOC/DOCX) или отправьте /cancel для отмены.'
);
return ctx.wizard.next();
},
async ctx => {
if (
ctx.message &&
'text' in ctx.message &&
ctx.from &&
ctx.message.text === '/cancel'
) {
await ctx.reply('Отправка резюме отменена.');
await ctx.reply(
'Меню',
Markup.keyboard([
['Моя анкета'],
['Вакансии'],
['Уведомления: включены'],
]).resize()
);
return ctx.scene.leave();
}
if (ctx.message && 'document' in ctx.message && ctx.from) {
const file = ctx.message.document;
const allowedMimeTypes = [
// 'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
];
if (!allowedMimeTypes.includes(file.mime_type || '')) {
await ctx.reply('Пожалуйста, отправьте файл в формате DOC или DOCX.');
return;
}
try {
const fileLink = await ctx.telegram.getFileLink(file.file_id);
const fileExtension =
file.mime_type === 'application/pdf'
? 'pdf'
: file.mime_type === 'application/msword'
? 'doc'
: 'docx';
const filePath = path.join(
__dirname,
`${file.file_id}.${fileExtension}`
);
// Загрузка файла на локальную машину
const response = await axios.get(fileLink.href, {
responseType: 'stream',
});
response.data
.pipe(fs.createWriteStream(filePath))
.on('finish', async () => {
const form = new FormData();
form.append('file', fs.createReadStream(filePath));
try {
const uploadResponse = await axios.post(
`${process.env.API}services/resume/`,
form,
{
headers: {
...form.getHeaders(),
'X-API-KEY':
'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
timeout: 10000,
}
);
const fileName = uploadResponse.data.filename;
await axios.patch(
`${process.env.API}students/${
ctx.from?.id
}?Link=${encodeURIComponent(fileName)}`,
{},
{
headers: {
'X-API-KEY':
'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
'Content-Type': 'application/json',
},
}
);
await ctx.reply('Резюме успешно загружено.');
await ctx.reply(
'Меню',
Markup.keyboard([
['Моя анкета'],
['Вакансии'],
['Уведомления: включены'],
]).resize()
);
return ctx.scene.leave();
} catch (uploadError) {
console.error('Ошибка при загрузке резюме на API:', uploadError);
await ctx.reply('Произошла ошибка при загрузке резюме.');
return ctx.scene.leave();
} finally {
fs.unlinkSync(filePath);
}
});
} catch (error) {
console.error('Ошибка при загрузке файла:', error);
await ctx.reply('Произошла ошибка при загрузке файла.');
}
} else {
await ctx.reply(
'Пожалуйста, отправьте файл в формате PDF, DOC или DOCX.'
);
}
}
);
// app.post('/api/resume', async (req: Request, res: Response) => {
// try {
// const postData = req.body;
// // Проверяем наличие chatId в теле запроса
// if (!postData.StudentID) {
// throw new Error('Chat ID is missing in the request body');
// }
// const [userInstance, created] = await Resume.findOrCreate({
// where: { id: postData.StudentID },
// defaults: {
// id: postData.StudentID || 0,
// name: postData.Name,
// group: postData.Group,
// type: postData.Type,
// skills: '',
// softskills: postData.Soft_skills,
// email: postData.Email,
// },
// });
// await UserBase.update(
// { resume_id: postData.StudentID },
// { where: { id: postData.StudentID } }
// );
// if (userInstance instanceof Resume) {
// // userInstance теперь имеет тип User
// if (created) {
// console.log('New user created:', userInstance);
// } else {
// console.log('User already exists:', userInstance);
// }
// console.log('Привет! Вы успешно зарегистрированы.');
// } else {
// console.error('Ошибка: userInstance не является экземпляром User.');
// }
// console.log('Received data:', postData);
// res.status(200).json({ message: 'Data received successfully' });
// } catch (error) {
// console.error(error);
// res.status(500).json({ error: 'Internal Server Error' });
// }
// });
app.post('/webhook', async (req: Request, res: Response) => {
try {
const postData = req.body;
const entityType = postData.data.entity_type;
const updatedMatches = postData.data.updated_matches;
console.log(postData);
if (entityType === 'job') {
for (const match of updatedMatches) {
const chatId = match.student_id;
const jobId = match.entity_id; // Получаем ID вакансии
const messageText =
'Ваше совпадение с вакансией было обновлено. Вот детали вакансии:';
if (!chatId) {
throw new Error('Неправильный Chat ID (student_id) в теле запроса.');
}
// Выполняем GET-запрос для получения данных о вакансии
const response = await axios.get(
`${process.env.API}jobs/${jobId}`, // Запрашиваем данные о вакансии по её ID
{
headers: {
'X-API-KEY':
'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
}
);
const vacancyData = response.data; // Данные о вакансии
console.log(vacancyData);
// Отправляем вакансию пользователю
await sendVacancyToUser(chatId, vacancyData);
console.log(
`Сообщение с вакансией отправлено пользователю с ID ${chatId}`
);
}
}
return res.status(200).json({ status: 'success' });
} catch (e) {
console.error('Error:', e);
res.status(500).json({ error: 'Произошла ошибка при отправке сообщения' });
}
});
// Функция для отправки вакансии пользователю
async function sendVacancyToUser(chatId: number, data: any) {
await bot.telegram.sendMessage(
chatId,
`<b>${data.Job_name}</b>\n\n` +
`<b>Компания:</b> ${data.Company_name}\n` +
`<b>Заработная плата:</b> ${data.Salary} руб/мес\n` +
`<b>Контактные данные:</b> ${data.Email}\n\n` +
`<b>Требования к кандидату:</b>\n` +
` - ${data.Year} курс\n` +
` - Опыт работы по специальности: ${data.Qualification}\n` +
` - Soft skills: ${data.Soft_skills}\n` +
`<b>Обязанности:</b>\n` +
`${data.Responsibilities}`,
{
parse_mode: 'HTML', // Используем HTML для форматирования текста
reply_markup: {
inline_keyboard: [
[
{
text: 'Сохранить вакансию',
callback_data: `savevacancy+${data.JobID}`,
},
],
],
},
}
);
}
// Обработчик для получения файла резюме
const stage = new Scenes.Stage<MyWizardContext>([resumeScene]);
bot.use(session());
bot.use(stage.middleware());
bot.use(menuSceneMiddleware);
events(bot);
accept(bot);
bot.action('uploadresume', async ctx => {
ctx.scene.enter('resumeScene');
ctx.answerCbQuery();
});
saveVacansy(bot);
bot
.launch({
webhook: {
domain: process.env.DOMAIN || '',
port: parseInt(process.env.HOOKPORT || ''),
},
})
.then(() =>
console.log(
'Webhook bot listening on port',
parseInt(process.env.HOOKPORT || '')
)
);
// bot.launch().then(() => {
// console.log('Бот запущен');
// });
app.listen(process.env.PORT, () => {
console.log(`Server is running at http://localhost:${process.env.PORT}`);
});

View File

@@ -0,0 +1,24 @@
{
"greeting": {
"photo": "",
"file": "",
"text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты",
"buttons": [
[
{ "text": "Понял", "callback": "accept" },
{ "text": "Понял, не запрашивать данные", "callback": "accept" }
]
]
},
"greeting2": {
"photo": "",
"file": "",
"text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты",
"buttons": [
[
{ "text": "Понял", "callback": "accept2" },
{ "text": "Понял, не запрашивать данные", "callback": "accept2" }
]
]
}
}

View File

@@ -0,0 +1,24 @@
{
"greeting": {
"photo": "",
"file": "",
"text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты",
"buttons": [
[
{ "text": "Понял", "callback": "accept" },
{ "text": "Понял, не запрашивать данные", "callback": "accept" }
]
]
},
"greeting2": {
"photo": "",
"file": "",
"text": "Вас приветствует бот MTUCI jobs! \n\nДля подтверждения того, что вы студент МТУСИ вам потребуется привязать вашу учетную запись lms к проекту MtuciTech (https://mtucitech.ru/) и привязать свой телеграм через бота @apimtucibot. Мы также можем запросить у них некоторые ваши данные, чтобы упростить заполнение анкеты",
"buttons": [
[
{ "text": "Понял", "callback": "accept2" },
{ "text": "Понял, не запрашивать данные", "callback": "accept2" }
]
]
}
}

View File

@@ -0,0 +1,23 @@
// Интерфейс, представляющий структуру пользователя
export interface User {
id: number; // Уникальный идентификатор пользователя
username: string; // Имя пользователя
password: string; // Количество очков пользователя
email: string;
isAuth:boolean;
points:number;
//referrerId?: number; // Идентификатор пригласившего пользователя (необязательно)
role: UserRole; // Роль пользователя (владелец, админ, модератор, клиент)
language: string; // Язык, на котором взаимодействует пользователь с ботом
message_id?: number;
last_obj?: string;
chat_id: number;
}
// Перечисление ролей пользователя
export enum UserRole {
OWNER = 'owner',
ADMIN = 'admin',
MODERATOR = 'moderator',
CLIENT = 'client',
}

View File

@@ -0,0 +1,43 @@
import { Context, Markup } from 'telegraf';
import { UserBase } from '../db/db';
import { MessagesService } from '../services/messagesService';
export class MenuController {
static async showLanguageMenu(ctx: Context): Promise<void> {
// await MessagesService.sendMessage(ctx, 'greeting');
}
static async setLanguage(ctx: Context): Promise<void> {
try {
if (!ctx.callbackQuery) {
throw new Error('CallbackQuery is not defined.');
}
const languageCode =
ctx.callbackQuery && 'data' in ctx.callbackQuery
? ctx.callbackQuery.data.split('_')[1]
: undefined;
if (languageCode) {
// Обновляем язык пользователя в базе данных
await UserBase.update(
{ language: languageCode },
{ where: { id: ctx.from?.id } }
);
// Обновляем язык в объекте контекста
if (ctx.from) {
ctx.from.language_code = languageCode;
}
} else {
// Если data отсутствует в callbackQuery, обработка не может быть выполнена
throw new Error('Missing data property in callbackQuery.');
}
} catch (error) {
console.error('Error during setLanguage:', error);
console.log(
'Произошла ошибка при обновлении языка. Пожалуйста, попробуйте снова позже.'
);
}
}
}

View File

@@ -0,0 +1,39 @@
import { Context, MiddlewareFn, Scenes } from 'telegraf';
import { UserBase } from '../db/db';
import { MessagesService } from '../services/messagesService';
// Middleware для обработки callback'ов
const callbackQueryMiddleware: MiddlewareFn<Scenes.WizardContext> = async (
ctx,
next
) => {
try {
const languageCode =
ctx.callbackQuery && 'data' in ctx.callbackQuery
? ctx.callbackQuery.data.split('_')[1]
: undefined;
// Обновляем язык пользователя в базе данных
if (languageCode && ctx.from?.id) {
await UserBase.update(
{ language: languageCode },
{ where: { id: ctx.from.id } }
);
// Отправляем сообщение об успешном обновлении языка
console.log(`Язык обновлен: ${languageCode}`);
await ctx.answerCbQuery();
await MessagesService.sendMessage(ctx, 'greeting');
}
} catch (error) {
console.error('Error during callback handling:', error);
console.log(
"Произошла ошибка при обработке callback'а. Пожалуйста, попробуйте снова позже."
);
}
// Вызываем следующий middleware
return next();
};
export const menuSceneMiddleware = callbackQueryMiddleware;

View File

@@ -0,0 +1,107 @@
import axios from 'axios';
import { Scenes, Telegraf } from 'telegraf';
import 'dotenv/config';
// const API_BASE_URL = 'https://mtucitech.ru/api/external/mtuci_jobs';
// const BEARER_TOKEN = 'zKbgaXQv{pvGtQm~U9$urtD#QsiHc@Ie';
export function accept(bot: Telegraf<Scenes.WizardContext>) {
bot.action('accept', async ctx => {
try {
const response = await axios.get(
`${process.env.MTUCI_TECH}/check?telegram_id=${ctx.from.id}`,
{
headers: {
Authorization: `Bearer ${process.env.BEARER_TOKEN}`,
},
}
);
const data = response.data;
// Логика обработки ответа
switch (data.status) {
case 338:
ctx.answerCbQuery(
'Ошибка на стороне сервера. Пожалуйста, сообщите нам.'
);
break;
case 339:
ctx.answerCbQuery('Пользователь не привязан к проекту.');
break;
case 340:
ctx.reply('Можете заполнить своё резюме', {
reply_markup: {
inline_keyboard: [
[
{
text: 'Резюме',
web_app: { url: process.env.WEB_APP || '' },
},
],
],
resize_keyboard: true,
one_time_keyboard: true,
},
});
ctx.answerCbQuery('Пользователь найден, но не привязан LMS.');
break;
case 341:
ctx.reply('Можете заполнить своё резюме', {
reply_markup: {
inline_keyboard: [
[
{
text: 'Резюме',
web_app: { url: process.env.WEB_APP || '' },
},
],
],
resize_keyboard: true,
one_time_keyboard: true,
},
});
ctx.answerCbQuery(
'Пользователь найден, LMS привязан. Можно запросить дополнительные данные.'
);
default:
ctx.answerCbQuery('Неизвестный статус.');
break;
}
} catch (error) {
if (axios.isAxiosError(error)) {
// Обработка ошибок Axios
if (error.response && error.response.status === 409) {
ctx.answerCbQuery('Вакансия уже добавлена в избранное');
} else {
ctx.answerCbQuery('Произошла ошибка');
console.error(error.response?.data || error.message);
}
} else {
// Обработка других типов ошибок
ctx.answerCbQuery('Произошла неизвестная ошибка');
console.error(error);
}
}
});
bot.action('accept2', async ctx => {
ctx.reply('Можете заполнить своё резюме', {
reply_markup: {
inline_keyboard: [
[
{
text: 'Резюме',
web_app: { url: process.env.WEB_APP || '' },
},
],
],
resize_keyboard: true,
one_time_keyboard: true,
},
});
ctx.answerCbQuery()
});
}

View File

@@ -0,0 +1,366 @@
import { Telegraf, Context, Scenes, Markup } from 'telegraf';
import { MessagesService } from '../../services/messagesService';
import { Resume, UserBase } from '../../db/db';
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import { MyWizardContext } from '../..';
interface Vacancies {
JobID: number;
Company_name: string;
Job_name: string;
Year: string;
Qualification: boolean;
Soft_skills: string;
Salary: number;
Email: string;
Archive: boolean;
Responsibilities: string;
}
let vacancies: Vacancies[] = [];
export function events(bot: Telegraf<Scenes.WizardContext>) {
bot.action('skip', async ctx => {
ctx.reply(
'Меню',
Markup.keyboard([
['Моя анкета'],
['Вакансии'],
// ['Уведомления: включены'],
]).resize()
);
ctx.answerCbQuery();
});
bot.hears('Вернуться в меню', async ctx => {
ctx.reply(
'Меню',
Markup.keyboard([
['Моя анкета'],
['Вакансии'],
// ['Уведомления: включены'],
]).resize()
);
});
bot.hears('Моя анкета', async ctx => {
try {
// Выполнение первого GET-запроса для получения данных пользователя
const response = await axios.get(
`${process.env.API}students/${ctx.from.id}`,
{
headers: {
'X-API-KEY':
'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
}
);
const data = response.data;
const resumeFile = await Resume.findOne({ where: { id: ctx.from.id } });
// Проверяем, существует ли резюме
if (resumeFile && resumeFile.resumefile.length > 1) {
// Выполнение второго GET-запроса для получения файла с резюме
const resumeResponse = await axios({
method: 'GET',
url: `${process.env.API}services/resume/${resumeFile.resumefile}`,
headers: {
'X-API-KEY':
'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
responseType: 'stream',
timeout: 10000,
});
// Сохраняем файл временно на диск
const resumePath = path.join(__dirname, 'resume.pdf');
const writer = fs.createWriteStream(resumePath);
resumeResponse.data.pipe(writer);
writer.on('finish', async () => {
// Отправка файла и сообщения с данными из API
await ctx.replyWithDocument({ source: resumePath });
await ctx.replyWithHTML(
`<b>Имя:</b> ${data.Name}\n<b>Группа:</b> ${
data.Group
}\n<b>Тип:</b> ${
data.Type
}\n<b>Готов на занятость:</b> ${data.Time.join()}\n<b>О себе:</b> ${
data.Soft_skills
}\n<b>Почта:</b> ${data.Email}\n`,
{
reply_markup: {
inline_keyboard: [
[
{
text: 'Редактировать анкету',
web_app: { url: process.env.WEB_APP || '' },
},
],
[
{
text: 'Обновить резюме',
callback_data: 'update_resume',
},
],
],
},
}
);
// Удаляем временный файл
fs.unlinkSync(resumePath);
});
writer.on('error', err => {
console.error('Ошибка при записи файла резюме:', err);
ctx.reply('Произошла ошибка при получении файла резюме.');
});
} else {
// Отправка только сообщения с данными из API, если резюме не существует
await ctx.replyWithHTML(
`<b>Имя:</b> ${data.Name}\n<b>Группа:</b> ${
data.Group
}\n<b>Тип:</b> ${
data.Type
}\n<b>Готов на занятость:</b> ${data.Time.join()}\n<b>О себе:</b> ${
data.Soft_skills
}\n<b>Почта:</b> ${data.Email}\n`,
{
reply_markup: {
inline_keyboard: [
[
{
text: 'Редактировать',
web_app: { url: process.env.WEB_APP || '' },
},
],
[
{
text: 'Загрузить резюме',
callback_data: 'update_resume',
},
],
],
},
}
);
}
} catch (error) {
console.error('Ошибка при выполнении запроса к API:', error);
ctx.reply('Произошла ошибка при получении данных из API.');
}
});
bot.hears('Вакансии', async ctx => {
try {
await ctx.reply(
'Выберите тип вакансии:',
Markup.keyboard([
['Актуальные вакансии'],
['Сохраненные вакансии'],
['Поиск'],
['Вернуться в меню'],
]).resize()
);
} catch (error) {
console.error('Ошибка при выполнении запроса к API:', error);
ctx.reply('Произошла ошибка при получении данных из API.');
}
});
bot.hears('Поиск', async ctx => {
try {
await ctx.reply(
'Поиск вакансий:',
Markup.keyboard([
['Актуальные вакансии'],
['Сохраненные вакансии'],
['Вернуться в меню'],
]).resize()
);
ctx.reply('Страница с поиском вакансий', {
reply_markup: {
inline_keyboard: [
[
{
text: 'Перейти',
web_app: { url: process.env.WEB_APP_SEARCH || '' },
},
],
],
resize_keyboard: true,
one_time_keyboard: true,
},
});
} catch (error) {
console.error('Ошибка при выполнении запроса к API:', error);
ctx.reply('Произошла ошибка при получении данных из API.');
}
});
bot.hears('Актуальные вакансии', async ctx => {
try {
await UserBase.update(
{ vacansyindex: 0 },
{
where: {
id: ctx.from.id,
},
}
);
// Выполнение первого GET-запроса для получения данных пользователя
const response = await axios.get(
`${process.env.API}students/matches/${ctx.from.id}`,
{
headers: {
'X-API-KEY':
'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
}
);
vacancies = response.data;
console.log(vacancies);
if (vacancies.length === 0) {
ctx.reply('На данный момент нет актуальных вакансий.');
return;
}
let user = await UserBase.findOne({ where: { id: ctx.from.id } });
if (user) {
let currentIndex = user?.vacansyindex;
await sendVacancy(ctx, currentIndex, true);
}
} catch (error) {
console.error('Ошибка при выполнении запроса к API:', error);
ctx.reply('Произошла ошибка при получении данных из API.');
}
});
bot.hears('Сохраненные вакансии', async ctx => {
try {
await UserBase.update(
{ vacansyindex: 0 },
{
where: {
id: ctx.from.id,
},
}
);
// Выполнение первого GET-запроса для получения данных пользователя
const response = await axios.get(
`${process.env.API}students/favourites/${ctx.from.id}`,
{
headers: {
'X-API-KEY':
'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
}
);
vacancies = response.data;
console.log(vacancies);
if (vacancies.length === 0) {
ctx.reply('На данный момент нет сохранённых вакансий.');
return;
}
let user = await UserBase.findOne({ where: { id: ctx.from.id } });
if (user) {
let currentIndex = user?.vacansyindex;
await sendVacancy(ctx, currentIndex, false);
}
} catch (error) {
console.error('Ошибка при выполнении запроса к API:', error);
ctx.reply('На данный момент нет сохранённых вакансий.');
// ctx.reply('Произошла ошибка при получении данных из API.');
}
});
bot.action('next', async ctx => {
let user = await UserBase.findOne({ where: { id: ctx.from.id } });
if (user) {
let currentIndex = user?.vacansyindex + 1;
if (currentIndex <= vacancies.length - 1) {
await UserBase.update(
{ vacansyindex: currentIndex },
{ where: { id: ctx.from.id } }
);
await sendVacancy(ctx, currentIndex, true);
ctx.answerCbQuery();
} else {
ctx.answerCbQuery('Это последняя вакансия.');
}
}
});
bot.action('update_resume', async ctx => {
await ctx.scene.enter('resumeScene');
await ctx.answerCbQuery();
});
bot.action('prev', async ctx => {
let user = await UserBase.findOne({ where: { id: ctx.from.id } });
if (user) {
let currentIndex = user?.vacansyindex - 1;
if (currentIndex >= 0) {
await UserBase.update(
{ vacansyindex: currentIndex },
{ where: { id: ctx.from.id } }
);
await sendVacancy(ctx, currentIndex, true);
ctx.answerCbQuery();
} else {
ctx.answerCbQuery('Это первая вакансия.');
}
}
});
async function sendVacancy(
ctx: MyWizardContext,
index: number,
showSaveButton: boolean
) {
const data = vacancies[index];
// Формируем кнопки навигации
let inlineKeyboard = [
[
{ text: 'Назад', callback_data: 'prev' },
{ text: `${index + 1}/${vacancies.length}`, callback_data: 'new' },
{ text: 'Далее', callback_data: 'next' },
],
];
// Добавляем кнопку "Сохранить вакансию" только если showSaveButton = true
if (showSaveButton) {
inlineKeyboard.push([
{
text: 'Сохранить вакансию',
callback_data: `savevacancy+${data.JobID}`,
},
]);
}
// Отправляем сообщение с вакансиями
await ctx.replyWithHTML(
`<b>${data.Job_name}</b>\n\n` +
`<b>Компания:</b> ${data.Company_name}\n` +
`<b>Заработная плата:</b> ${data.Salary} руб/мес\n` +
`<b>Контактные данные:</b> ${data.Email}\n\n` +
`<b>Требования к кандидату:</b>\n` +
` - ${data.Year} курс\n` +
` - Опыт работы по специальности: ${data.Qualification}\n` +
` - Soft skills: ${data.Soft_skills}\n` +
`<b>Обязанности:</b>\n` +
`${data.Responsibilities}`,
{
reply_markup: {
inline_keyboard: inlineKeyboard,
},
}
);
}
}

View File

@@ -0,0 +1,49 @@
import { Telegraf, Context, Scenes, Markup } from 'telegraf';
import { MessagesService } from '../../services/messagesService';
import { Resume, UserBase } from '../../db/db';
import axios from 'axios';
import 'dotenv/config';
import fs from 'fs';
import path from 'path';
import { MyWizardContext } from '../..';
export function saveVacansy(bot: Telegraf<Scenes.WizardContext>) {
bot.on('callback_query', async ctx => {
if (ctx.callbackQuery && 'data' in ctx.callbackQuery) {
const callbackData = ctx.callbackQuery.data;
if (callbackData.startsWith('savevacancy+')) {
const jobID = callbackData.split('+')[1];
try {
const data = await axios.post(
`${process.env.API}students/favourites/`,
{ StudentID: ctx.from.id, JobID: jobID },
{
headers: {
'X-API-KEY':
'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
}
);
console.log(data);
ctx.answerCbQuery('Вакансия была сохранена');
} catch (error) {
if (axios.isAxiosError(error)) {
// Обработка ошибок Axios
if (error.response && error.response.status === 409) {
ctx.answerCbQuery('Вакансия уже добавлена в избранное');
} else {
ctx.answerCbQuery('Произошла ошибка при сохранении вакансии');
console.error(error.response?.data || error.message);
}
} else {
// Обработка других типов ошибок
ctx.answerCbQuery('Произошла неизвестная ошибка');
console.error(error);
}
}
}
}
});
}

View File

@@ -0,0 +1,371 @@
// Импортируем необходимые модули
import * as fs from 'fs';
// import { CustomContext } from '../types/telegram';
import path from 'path';
import { UserBase } from '../db/db';
import { Context, Markup } from 'telegraf';
// Определяем тип для объекта сообщений
type Button = {
text: string;
callback: string;
url?: string;
};
// Определяем тип для объекта контента сообщения
type Message = {
text: string;
photo?: string; // Путь к фотографии
file?: string;
buttons?: Button[][];
};
// Определяем тип для объекта сообщений
type Messages = { [key: string]: Message };
export class MessagesService {
private static messages: { [lang: string]: Messages } = {};
public static loadMessages(lang: string): Messages {
if (!this.messages[lang]) {
const filePath = path.join(__dirname, `../languages/${lang}.json`);
const data = fs.readFileSync(filePath, 'utf-8');
this.messages[lang] = JSON.parse(data);
}
return this.messages[lang];
}
public static async backMessage(ctx: Context) {
try {
const user = (await UserBase.findOne({
where: {
id: ctx.from?.id,
},
})) as {
id: number;
username: string;
password: string;
email: string;
isAuth: boolean;
points: number;
role: string;
language: string;
message_id: number;
last_obj: string;
chat_id: number;
} | null;
if (user !== null && ctx.chat) {
await ctx.telegram.copyMessage(
ctx.chat?.id,
ctx.chat?.id,
user.message_id - 1
);
} else {
console.log('error');
}
} catch (error) {
console.log(error);
}
}
public static async hiddenMessage(ctx: Context, key: string) {
try {
const user = (await UserBase.findOne({
where: {
id: ctx.from?.id,
},
})) as {
id: number;
username: string;
password: string;
email: string;
isAuth: boolean;
points: number;
role: string;
language: string;
message_id: number;
last_obj: string;
chat_id: number;
} | null;
if (user && user.message_id && ctx.callbackQuery?.message) {
await ctx.telegram.deleteMessage(
ctx.chat?.id || 0,
ctx.callbackQuery?.message?.message_id
);
console.log('Hidden message deleted successfully.');
await UserBase.update(
{ last_obj: key }, // Новое значение
{
where: {
id: ctx.from?.id, // Условие для выбора записей
},
}
);
} else {
console.log('User or message not found.');
}
} catch (error) {
console.error('Error deleting last message:', error);
ctx.reply(
'An error occurred while deleting the last message. Please try again later.'
);
}
}
public static async deleteLastMessage(ctx: Context) {
try {
const user = (await UserBase.findOne({
where: {
id: ctx.from?.id,
},
})) as {
id: number;
username: string;
password: string;
email: string;
isAuth: boolean;
points: number;
role: string;
language: string;
message_id: number;
last_obj: string;
chat_id: number;
} | null;
if (user && user.message_id && ctx.callbackQuery?.message) {
await ctx.telegram.deleteMessage(
ctx.chat?.id || 0,
ctx.callbackQuery?.message?.message_id
);
console.log('Last message deleted successfully.');
await UserBase.update(
{ message_id: ctx.callbackQuery?.message?.message_id }, // Новое значение
{
where: {
id: ctx.from?.id, // Условие для выбора записей
},
}
);
} else {
console.log('User or message not found.');
}
} catch (error) {
console.error('Error deleting last message:', error);
ctx.reply(
'An error occurred while deleting the last message. Please try again later.'
);
}
}
public static async sendMessage(
ctx: Context,
key: string
) {
try {
const user = (await UserBase.findOne({
where: {
id: ctx.from?.id,
},
})) as {
id: number;
username: string;
password: string;
email: string;
isAuth: boolean;
points: number;
role: string;
language: string;
message_id: number;
last_obj: string;
chat_id: number;
} | null;
if (user !== null) {
const lang = user.language;
const messages = this.loadMessages(lang);
const message = messages[key];
if (message) {
// Определяем, есть ли у сообщения фотография
const hasPhoto = message.photo !== '';
const hasFile = message.file !== '';
if (
hasPhoto &&
message.buttons &&
message.buttons.length > 0 &&
message.photo
) {
const photoPath = path.join(__dirname, message.photo);
if (message.buttons && message.buttons.length > 0) {
const rows: Button[][] = message.buttons;
if (
Array.isArray(rows) &&
rows.every(row => Array.isArray(row))
) {
const sentMessage = await ctx.replyWithPhoto(
{ source: fs.createReadStream(photoPath) || '' },
{
protect_content: true,
caption: message.text,
parse_mode: 'HTML',
...Markup.inlineKeyboard(
rows.map(row =>
row.map(button => {
if (button.url) {
return Markup.button.url(button.text, button.url);
} else {
return Markup.button.callback(
button.text,
button.callback
);
}
})
)
),
}
);
if (sentMessage.message_id) {
await UserBase.update(
{ message_id: sentMessage.message_id },
{
where: {
id: ctx.from?.id,
},
}
);
}
} else {
console.error('rows должен быть массивом массивов');
}
}
} else if (hasPhoto && message.photo) {
const photoPath = path.join(__dirname, message.photo);
const sentMessage = await ctx.replyWithPhoto(
{ source: fs.createReadStream(photoPath) || '' },
{
protect_content: true,
caption: message.text,
parse_mode: 'HTML',
}
);
if (sentMessage.message_id) {
await UserBase.update(
{ message_id: sentMessage.message_id },
{
where: {
id: ctx.from?.id,
},
}
);
}
} else if (hasFile && message.file) {
const pdfFilePath = path.join(__dirname, message.file);
const fileName = path.basename(pdfFilePath);
// Отправляем файл
const sentMessage = await ctx.replyWithDocument(
{
source: fs.createReadStream(pdfFilePath) || '',
filename: fileName,
},
{
protect_content: true,
caption: message.text,
parse_mode: 'HTML',
}
);
if (sentMessage.message_id) {
await UserBase.update(
{ message_id: sentMessage.message_id },
{
where: {
id: ctx.from?.id,
},
}
);
}
} else {
// Отправляем текст и кнопки, если они есть
if (message.buttons && message.buttons.length > 0) {
const rows: Button[][] = message.buttons;
if (
Array.isArray(rows) &&
rows.every(row => Array.isArray(row))
) {
const sentMessage = await ctx.reply(message.text, {
protect_content: true,
parse_mode: 'HTML',
...Markup.inlineKeyboard(
rows.map(row =>
row.map(button => {
if (button.url) {
return Markup.button.url(button.text, button.url);
} else {
return Markup.button.callback(
button.text,
button.callback
);
}
})
)
),
});
if (sentMessage.message_id) {
await UserBase.update(
{ message_id: sentMessage.message_id },
{
where: {
id: ctx.from?.id,
},
}
);
}
} else {
console.error('rows должен быть массивом массивов');
}
} else {
// Отправляем только текст, если нет кнопок
const sentMessage = await ctx.replyWithHTML(message.text, {
protect_content: true,
});
await UserBase.update(
{ message_id: ctx.callbackQuery?.message?.message_id },
{
where: {
id: ctx.from?.id,
},
}
);
if (sentMessage.message_id) {
await UserBase.update(
{ message_id: sentMessage.message_id },
{
where: {
id: ctx.from?.id,
},
}
);
}
}
}
} else {
console.error(`Key not found: ${key}`);
}
} else {
console.error('User not found.');
}
} catch (error) {
console.error('Error fetching user language:', error);
console.log('An error occurred. Please try again later.');
}
}
}

109
mtucijobsbot/tsconfig.json Normal file
View File

@@ -0,0 +1,109 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}