From 9c9dd974b87cda3b55e9626299ce60a9eddab1c6 Mon Sep 17 00:00:00 2001 From: zamelane Date: Wed, 27 Nov 2024 10:40:35 +0700 Subject: [PATCH 01/12] =?UTF-8?q?=D0=9D=D0=B0=D0=B1=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D1=83=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9=20=D0=BE=D0=B1=20=D0=BE=D1=86=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=B0=D1=85=20=D0=B2=20=D1=82=D0=B5=D0=BB=D0=B5=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/.env.example | 1 + apps/api/package.json | 2 + apps/api/src/config/index.ts | 3 +- apps/api/src/config/params.ts | 3 +- apps/api/src/config/types.ts | 1 + .../api/src/helpers/notificationController.ts | 24 +++ apps/api/src/main.ts | 2 +- .../models/Mark/actions/save/markSaveOrGet.ts | 22 ++- .../models/Schedule/actions/save/daySave.ts | 5 +- .../Schedule/actions/save/lessonSave.ts | 8 +- apps/api/src/models/Subscribe/index.ts | 1 + apps/api/src/models/Subscribe/model.ts | 34 ++++ .../Task/actions/save/tasksSaveOrGet.ts | 28 +++- apps/api/src/models/relations.ts | 4 + .../routes/lessons/service/get/getLessons.ts | 8 +- .../performance.current/service/index.ts | 10 +- .../service/save/savePerformance.ts | 6 +- apps/api/src/telegramBot.ts | 6 + apps/api/src/worker/index.ts | 4 +- apps/api/src/worker/notificator/config.ts | 2 + apps/api/src/worker/notificator/index.ts | 49 ++++++ .../src/worker/notificator/messages/index.ts | 153 ++++++++++++++++++ bun.lockb | Bin 147600 -> 206720 bytes 23 files changed, 353 insertions(+), 23 deletions(-) create mode 100644 apps/api/src/helpers/notificationController.ts create mode 100644 apps/api/src/models/Subscribe/index.ts create mode 100644 apps/api/src/models/Subscribe/model.ts create mode 100644 apps/api/src/telegramBot.ts create mode 100644 apps/api/src/worker/notificator/config.ts create mode 100644 apps/api/src/worker/notificator/index.ts create mode 100644 apps/api/src/worker/notificator/messages/index.ts diff --git a/apps/api/.env.example b/apps/api/.env.example index d08d51fb..4800b49c 100755 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -12,6 +12,7 @@ DATABASE_PORT=5432 DATABASE_NAME='databaseName' DATABASE_USERNAME='user' DATABASE_PASSWORD='password' +BOT_TOKEN='' POSTGRES_USER=postgres POSTGRES_PW=postgres diff --git a/apps/api/package.json b/apps/api/package.json index 20a68bc6..6981ee0b 100755 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -17,12 +17,14 @@ "dependencies": { "@diary-spo/crypto": "workspace:*", "@elysiajs/cors": "^1.1.1", + "@types/node-telegram-bot-api": "^0.64.7", "cache-manager": "^5.7.6", "elysia": "1.1.21", "elysia-compression": "^0.0.7", "elysia-helmet": "^2.0.0", "ky": "^1.7.2", "node-rsa": "^1.1.1", + "node-telegram-bot-api": "^0.66.0", "pg": "^8.13.1", "pg-hstore": "^2.3.4", "rand-token": "^1.0.1", diff --git a/apps/api/src/config/index.ts b/apps/api/src/config/index.ts index e92ff95a..d01b8414 100755 --- a/apps/api/src/config/index.ts +++ b/apps/api/src/config/index.ts @@ -22,5 +22,6 @@ export const { DATABASE_USERNAME, DATABASE_PASSWORD, TIMEZONE, - KEY_SCAN_PATH + KEY_SCAN_PATH, + BOT_TOKEN } = PARAMS_INIT diff --git a/apps/api/src/config/params.ts b/apps/api/src/config/params.ts index f41aff47..35d909b9 100755 --- a/apps/api/src/config/params.ts +++ b/apps/api/src/config/params.ts @@ -10,5 +10,6 @@ export const PARAMS_INIT: ParamsInit = { DATABASE_USERNAME: '', DATABASE_PASSWORD: '', TIMEZONE: getTimezone(), - KEY_SCAN_PATH: './' + KEY_SCAN_PATH: './', + BOT_TOKEN: '' } diff --git a/apps/api/src/config/types.ts b/apps/api/src/config/types.ts index 52ea4f8f..6f8d11e1 100755 --- a/apps/api/src/config/types.ts +++ b/apps/api/src/config/types.ts @@ -29,6 +29,7 @@ export interface StringParams { DATABASE_PASSWORD: string TIMEZONE: string KEY_SCAN_PATH: string + BOT_TOKEN: string } export type ParamsKeys = StringKeys diff --git a/apps/api/src/helpers/notificationController.ts b/apps/api/src/helpers/notificationController.ts new file mode 100644 index 00000000..0a708fe4 --- /dev/null +++ b/apps/api/src/helpers/notificationController.ts @@ -0,0 +1,24 @@ +import type { MarkKeys, Task } from '@diary-spo/shared' + +export interface IMarkEvent { + diaryUserId: bigint + task: Task + mark: MarkKeys + previousMarkId: number | null + status: 'ADD' | 'DELETE' | 'UPDATE' + eventDatetime: Date +} + +const ids_marks: IMarkEvent[] = [] // Идентификаторы оценок + +/** + * Регистрирует событие взаимодействия с оценкой + * @param event + */ +export const addNewMarkEvent = (event: IMarkEvent): void => { + ids_marks.push(event) + console.log(event) +} + +export const getMarkEvent = () => + ids_marks.length > 0 ? ids_marks.splice(0, 1)[0] : null diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index cbc858f7..f5dabae0 100755 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -4,7 +4,7 @@ import { Elysia } from 'elysia' import { compression } from 'elysia-compression' import { helmet } from 'elysia-helmet' -import { TIMEZONE } from '@config' +import { BOT_TOKEN, TIMEZONE } from '@config' import { sequelize } from '@db' import { getTimezone } from './config/getTimeZone' diff --git a/apps/api/src/models/Mark/actions/save/markSaveOrGet.ts b/apps/api/src/models/Mark/actions/save/markSaveOrGet.ts index 0d3e7b5b..dfdb759a 100755 --- a/apps/api/src/models/Mark/actions/save/markSaveOrGet.ts +++ b/apps/api/src/models/Mark/actions/save/markSaveOrGet.ts @@ -1,6 +1,7 @@ -import type { MarkKeys } from '@diary-spo/shared' +import type { MarkKeys, Task } from '@diary-spo/shared' import { type ICacheData, retriesForError } from '@helpers' import { formatDate } from '@utils' +import { addNewMarkEvent } from '../../../../helpers/notificationController' import { markValueSaveOrGet } from '../../../MarkValue' import type { IScheduleModel } from '../../../Schedule' import { markSaveOrGetUnSafe } from './markSaveOrGetUnSafe' @@ -10,7 +11,9 @@ export const markSaveOrGet = async ( schedule: IScheduleModel, taskId: bigint, termId: bigint | null, - authData: ICacheData + authData: ICacheData, + task: Task | null, + systemInitiator = false ) => { // Не сохраняем текущий семестр для предыдущих семестров if (authData.termStartDate && authData.termLastUpdate) { @@ -35,11 +38,26 @@ export const markSaveOrGet = async ( authData ]) + const previousMarkId = markDB[0].markValueId + // Если есть изменения, то он потом сохранит markDB[0].markValueId = markValueId if (termId) { markDB[0].termId = termId } + // Сохраняем событие добавления/обновления оценки + if (task && systemInitiator) + if (markDB[0].markValueId !== previousMarkId || markDB[1]) + addNewMarkEvent({ + mark, + previousMarkId: + markDB[0].markValueId !== previousMarkId ? previousMarkId : null, + task, + diaryUserId: authData.localUserId, + status: markDB[1] ? 'ADD' : 'UPDATE', + eventDatetime: new Date() + }) + return markDB[0].save() } diff --git a/apps/api/src/models/Schedule/actions/save/daySave.ts b/apps/api/src/models/Schedule/actions/save/daySave.ts index 14145e4d..e573f4dc 100755 --- a/apps/api/src/models/Schedule/actions/save/daySave.ts +++ b/apps/api/src/models/Schedule/actions/save/daySave.ts @@ -8,7 +8,8 @@ import { lessonSave } from './lessonSave' export const daySave = async ( day: Day, authData: ICacheData, - termPromise?: ITermDetectP + termPromise?: ITermDetectP, + systemInitiator = false ) => { if (!day.lessons) { return @@ -19,7 +20,7 @@ export const daySave = async ( for (const lesson of day.lessons) { const promise = retriesForError( lessonSave, - [new Date(day.date), lesson, authData, termPromise], + [new Date(day.date), lesson, authData, termPromise, systemInitiator], 3, 1000 ) //lessonSave(day.date, lesson, authData, termPromise) diff --git a/apps/api/src/models/Schedule/actions/save/lessonSave.ts b/apps/api/src/models/Schedule/actions/save/lessonSave.ts index df5860cf..d7d75442 100755 --- a/apps/api/src/models/Schedule/actions/save/lessonSave.ts +++ b/apps/api/src/models/Schedule/actions/save/lessonSave.ts @@ -27,13 +27,15 @@ import type { ScheduleWhere } from '../types' * @param lesson - Занятие для сохранения * @param authData - Данные, предоставленные клиентом для авторизации * @param termPromise + * @param systemInitiator * @returns Promise */ export const lessonSave = async ( date: Date, lesson: Lesson, authData: ICacheData, - termPromise?: ITermDetectP + termPromise?: ITermDetectP, + systemInitiator = false ): Promise => { /** * Пропускаем пустые занятия типа: @@ -124,7 +126,7 @@ export const lessonSave = async ( /** * Если расписание есть в базе, то - * пробуем поменять поля и если они поменялись, то + * пробуем сравнить поля и если они поменялись, то * он (метод save) сделает запрос для сохранения */ if (!isCreated) { @@ -165,7 +167,7 @@ export const lessonSave = async ( const schedule = await promiseToReturn retriesForError( tasksSaveOrGet, - [gradebook.tasks, schedule, authData, termPromise], + [gradebook.tasks, schedule, authData, termPromise, systemInitiator], 2 ) } diff --git a/apps/api/src/models/Subscribe/index.ts b/apps/api/src/models/Subscribe/index.ts new file mode 100644 index 00000000..116e6686 --- /dev/null +++ b/apps/api/src/models/Subscribe/index.ts @@ -0,0 +1 @@ +export * from './model' diff --git a/apps/api/src/models/Subscribe/model.ts b/apps/api/src/models/Subscribe/model.ts new file mode 100644 index 00000000..4ccbdebf --- /dev/null +++ b/apps/api/src/models/Subscribe/model.ts @@ -0,0 +1,34 @@ +import { DataTypes } from 'sequelize' + +import { cache, enableCache, sequelize } from '@db' + +import { DiaryUserModel } from '../DiaryUser' +import type { IModelPrototypeNoId } from '../types' + +export type SubscribeModelType = { + diaryUserId: bigint + tgId: bigint +} + +export type ISubscribeModelType = IModelPrototypeNoId + +const subscribeModel = sequelize.define('subscribe', { + diaryUserId: { + type: DataTypes.BIGINT, + primaryKey: true, + references: { + model: DiaryUserModel, + key: 'id' + } + }, + tgId: { + type: DataTypes.BIGINT, + primaryKey: true, + allowNull: false, + unique: true + } +}) + +export const SubscribeModel = enableCache + ? cache.init(subscribeModel) + : subscribeModel diff --git a/apps/api/src/models/Task/actions/save/tasksSaveOrGet.ts b/apps/api/src/models/Task/actions/save/tasksSaveOrGet.ts index 1a72a686..de42e9c7 100755 --- a/apps/api/src/models/Task/actions/save/tasksSaveOrGet.ts +++ b/apps/api/src/models/Task/actions/save/tasksSaveOrGet.ts @@ -3,6 +3,7 @@ import { type ICacheData, retriesForError } from '@helpers' import { objPropertyCopy } from 'src/helpers/objPropertyCopy' import { taskTypeSaveOrGet } from 'src/models/TaskType' +import { addNewMarkEvent } from '../../../../helpers/notificationController' import { markDelete, markSaveOrGet } from '../../../Mark' import { requiredSaveOrGet } from '../../../Required' import type { IScheduleModel } from '../../../Schedule' @@ -14,7 +15,8 @@ export const tasksSaveOrGet = async ( tasks: Task[], schedule: IScheduleModel, authData: ICacheData, - termPromise?: ITermDetectP + termPromise?: ITermDetectP, + systemInitiator = false ) => { const promises = [] const scheduleId = schedule.id @@ -80,9 +82,29 @@ export const tasksSaveOrGet = async ( // На всякий який (ну и пустышки не берём) if (task.mark) { - markSaveOrGet(task.mark, schedule, taskId, termId, authData) + markSaveOrGet( + task.mark, + schedule, + taskId, + termId, + authData, + task, + systemInitiator + ) } else { - markDelete(taskId, authData) + markDelete(taskId, authData).then((count) => { + // Игнорируем не системные инициализаторы (т.е. если пользователь уже сам посмотрел) + if (count > 0 && systemInitiator) + // Регистрируем событие удаления оценки + addNewMarkEvent({ + mark: task.mark, + task, + diaryUserId: authData.localUserId, + status: 'DELETE', + eventDatetime: new Date(), + previousMarkId: null + }) + }) } return taskReturn diff --git a/apps/api/src/models/relations.ts b/apps/api/src/models/relations.ts index b01a203c..93ab5623 100755 --- a/apps/api/src/models/relations.ts +++ b/apps/api/src/models/relations.ts @@ -18,6 +18,7 @@ import { ScheduleModel } from './Schedule' import { ScheduleSubgroupModel } from './ScheduleSubgroup' import { SubgroupModel } from './Subgroup' import { SubjectModel } from './Subject' +import { SubscribeModel } from './Subscribe' import { TaskModel } from './Task' import { TaskTypeModel } from './TaskType' import { TeacherModel } from './Teacher' @@ -192,6 +193,9 @@ TermSubjectModel.belongsTo(TeacherModel) DiaryUserModel.hasMany(TermSubjectModel) TermSubjectModel.belongsTo(DiaryUserModel) +DiaryUserModel.hasMany(SubscribeModel) +SubscribeModel.belongsTo(DiaryUserModel) + if (forceSyncDatabase) { console.log('Syncing database...') await sequelize.sync({}) diff --git a/apps/api/src/routes/lessons/service/get/getLessons.ts b/apps/api/src/routes/lessons/service/get/getLessons.ts index a8274a72..ef401d5c 100755 --- a/apps/api/src/routes/lessons/service/get/getLessons.ts +++ b/apps/api/src/routes/lessons/service/get/getLessons.ts @@ -12,8 +12,9 @@ export const getLessonsService = async ( startDate: string, endDate: string, authData: ICacheData, - notGetFromDB = false -): Promise => { + notGetFromDB = false, + systemInitiator = false +): Promise => { const path = `${SERVER_URL}/services/students/${authData.idFromDiary}/lessons/${startDate}/${endDate}` const response = await fetcher.get(path, { @@ -25,6 +26,7 @@ export const getLessonsService = async ( } if (!response.ok && !notGetFromDB) { + if (systemInitiator) return null // Получаем из базы const rawSchedule = await ScheduleGetFromDB(startDate, endDate, authData) return getFormattedResponse(rawSchedule, startDate, endDate, authData) @@ -36,7 +38,7 @@ export const getLessonsService = async ( for (const day of days) { const backgroundProcess = async () => - daySave(day, authData, term).catch((err: string) => + daySave(day, authData, term, systemInitiator).catch((err: string) => console.error(`Ошибка сохранения расписания: ${err}`) ) diff --git a/apps/api/src/routes/performance.current/service/index.ts b/apps/api/src/routes/performance.current/service/index.ts index 8003f20b..57df1b77 100755 --- a/apps/api/src/routes/performance.current/service/index.ts +++ b/apps/api/src/routes/performance.current/service/index.ts @@ -5,19 +5,21 @@ import { getPerformanceCurrent } from './get' import { savePerformance } from './save' export const getCurrPerformance = async ( - authData: ICacheData -): Promise => { + authData: ICacheData, + systemInitiator = false // Инициировано системой и нужно ли обязательно брать только из сетевого города ? +): Promise => { const response = await getPerformanceCurrent(authData) if (!response.ok) { + if (systemInitiator) return null // Возвращаем из базы return getPerformanceFromDB(authData) } - const result = await response.json() + const result = await response.json() // Сохраняем в базе - savePerformance(result, authData) + savePerformance(result, authData, systemInitiator) return result } diff --git a/apps/api/src/routes/performance.current/service/save/savePerformance.ts b/apps/api/src/routes/performance.current/service/save/savePerformance.ts index 34ba3e52..50f9d837 100644 --- a/apps/api/src/routes/performance.current/service/save/savePerformance.ts +++ b/apps/api/src/routes/performance.current/service/save/savePerformance.ts @@ -10,7 +10,8 @@ import { getPerformanceFromDB } from '../get' */ export const savePerformance = async ( performance: PerformanceCurrent, - authData: ICacheData + authData: ICacheData, + systemInitiator = false ) => { // Вытягиваем данные из БД, чтобы потом сравнивать const dataFromDatabase = await getPerformanceFromDB(authData) @@ -63,7 +64,8 @@ export const savePerformance = async ( String(group.startDate), String(group.endDate), authData, - true + true, + systemInitiator ) } } diff --git a/apps/api/src/telegramBot.ts b/apps/api/src/telegramBot.ts new file mode 100644 index 00000000..9e0365c3 --- /dev/null +++ b/apps/api/src/telegramBot.ts @@ -0,0 +1,6 @@ +import { BOT_TOKEN } from '@config' +import TelegramBot from 'node-telegram-bot-api' + +const token = BOT_TOKEN + +export const bot = new TelegramBot(token, { polling: true }) diff --git a/apps/api/src/worker/index.ts b/apps/api/src/worker/index.ts index 12f111da..76b26aad 100755 --- a/apps/api/src/worker/index.ts +++ b/apps/api/src/worker/index.ts @@ -1,11 +1,13 @@ import { error } from '@utils' import { sleep } from 'bun' import { cookieUpdater } from './cookieUpdater' +import { notificatorChecker } from './notificator' +import '../models/relations' async function runWorker(): Promise { while (true) { try { - await Promise.all([cookieUpdater()]) + await Promise.all([cookieUpdater(), notificatorChecker()]) await sleep(1000) // разгружаем немного } catch (e) { error('Error at worker', e) diff --git a/apps/api/src/worker/notificator/config.ts b/apps/api/src/worker/notificator/config.ts new file mode 100644 index 00000000..5effb64c --- /dev/null +++ b/apps/api/src/worker/notificator/config.ts @@ -0,0 +1,2 @@ +// Интервал повторения перепроверки новых оценок +export const INTERVAL_RUN = 2 * 60 // каждые 2 минуты diff --git a/apps/api/src/worker/notificator/index.ts b/apps/api/src/worker/notificator/index.ts new file mode 100644 index 00000000..7fbdb9c8 --- /dev/null +++ b/apps/api/src/worker/notificator/index.ts @@ -0,0 +1,49 @@ +import { getCookieFromToken } from '@helpers' +import { getMarkEvent } from '../../helpers/notificationController' +import { AuthModel } from '../../models/Auth' +import { DiaryUserModel } from '../../models/DiaryUser' +import { SubscribeModel } from '../../models/Subscribe' +import { getCurrPerformance } from '../../routes/performance.current/service' +import { checkInterval } from '../utils/checkInterval' +import { logger } from '../utils/logger' +import { INTERVAL_RUN } from './config' +import { sendEvent } from './messages' + +let lastRunning: Date | null = null +const log = logger('notificator') + +export const notificatorChecker = async (): Promise => { + if (lastRunning && !checkInterval(lastRunning, INTERVAL_RUN)) { + return + } + + lastRunning = new Date() + + log(`Запускаю обработчика уведомлений... ${new Date().toISOString()}`) + + // TODO: извлекать только подписчиков + const subscribes = await SubscribeModel.findAll() // Извлекаем всех юзеров + + for (const subscribe of subscribes) { + const auth = await AuthModel.findOne({ + where: { + diaryUserId: subscribe.diaryUserId + } + }) + + if (!auth) continue + + const cacheData = await getCookieFromToken(auth.token) + + await getCurrPerformance(cacheData, true) + } + + let markEvent = getMarkEvent() + console.log(markEvent) + + while (markEvent) { + await sendEvent(markEvent) + // Запрашиваем следующие событие + markEvent = getMarkEvent() + } +} diff --git a/apps/api/src/worker/notificator/messages/index.ts b/apps/api/src/worker/notificator/messages/index.ts new file mode 100644 index 00000000..32c2cc5f --- /dev/null +++ b/apps/api/src/worker/notificator/messages/index.ts @@ -0,0 +1,153 @@ +import { Grade } from '@diary-spo/shared' +import { getCookieFromToken } from '@helpers' +import { bot } from 'src/telegramBot' +import type { IMarkEvent } from '../../../helpers/notificationController' +import { DiaryUserModel } from '../../../models/DiaryUser' +import { MarkModel } from '../../../models/Mark' +import { MarkValueModel } from '../../../models/MarkValue' +import { ScheduleModel } from '../../../models/Schedule' +import { SubjectModel } from '../../../models/Subject' +import { SubscribeModel } from '../../../models/Subscribe' +import { TaskModel } from '../../../models/Task' + +bot.on('message', async (msg) => { + const chatId = msg.chat.id + + if (!msg.text) bot.sendMessage(chatId, 'Такое сообщение не поддерживается') + + const command = (msg.text ?? '').split(' ') + + switch (command[0]) { + case '/subscribe': { + if (command.length < 2) { + bot.sendMessage( + chatId, + 'Передайте вторым параметром актуальный токен, чтобы подписаться на уведомления' + ) + return + } + const token = command[1] + + const auth = await getCookieFromToken(token).catch(() => null) + + if (!auth) { + bot.sendMessage(chatId, 'Токен не найден ...') + return + } + + const subscribes = await SubscribeModel.findAll({ + where: { + tgId: chatId + } + }) + + if (subscribes.length >= 1) { + bot.sendMessage( + chatId, + 'Вы уже подписаны для других аккаунтов. Сначала отпишитесь от остальных (/unsubscribe)' + ) + return + } + + await SubscribeModel.create({ + diaryUserId: auth.localUserId, + tgId: BigInt(chatId) + }) + + const user = await DiaryUserModel.findOne({ + where: { + id: auth.localUserId + } + }) + + bot.sendMessage( + chatId, + `Вы подписались на аккаунт c ФИО => ${user?.firstName} ${user?.lastName} ${user?.middleName}` + ) + break + } + case '/unsubscribe': + await SubscribeModel.destroy({ + where: { + tgId: chatId + } + }) + bot.sendMessage( + chatId, + 'Вы успешно отписались от всех аккаунтов. Можете привязать новый (/subscribe)' + ) + break + default: + bot.sendMessage(chatId, 'Такой команды нету') + break + } +}) + +export const sendEvent = async (event: IMarkEvent) => { + const subscribe = await SubscribeModel.findOne({ + where: { + diaryUserId: event.diaryUserId + } + }) + + if (!subscribe) return + + let message = '' + + switch (event.status) { + case 'ADD': + message += 'НОВАЯ ОЦЕНКА!' + break + case 'UPDATE': + message += 'ВАМ ИЗМЕНИЛИ ОЦЕНКУ!' + break + case 'DELETE': + message += 'ВАМ УБРАЛИ ОЦЕНКУ!' + break + } + + const task = await TaskModel.findOne({ + where: { + idFromDiary: event.task.id + } + }) + + if (!task) return + + const schedule = await ScheduleModel.findOne({ + where: { + id: task.scheduleId + } + }) + + if (!schedule || !schedule?.subjectId) return + + const subject = await SubjectModel.findOne({ + where: { + id: schedule.subjectId + } + }) + + if (!subject) return + + const previousMark = event.previousMarkId + ? await MarkValueModel.findOne({ + where: { + id: event.previousMarkId + } + }) + : null + + message += ` + Оценка: ${Grade[event.mark] + (previousMark ? ` (предыдущая ${Grade[previousMark.value]})` : '')}, + Предмет: ${subject.name} (дата: ${schedule.date}) + + Задание + Тема: ${event.task.topic ?? '[НЕ ЗАДАНО]'} + Описание: ${event.task.condition ?? '[НЕ ЗАДАНО]'} + + Время обнаружения: ${event.eventDatetime.toUTCString()} + ` + + bot.sendMessage(String(subscribe.tgId), message) +} diff --git a/bun.lockb b/bun.lockb index 56d01f697eae1a25a3cd94e7633e0e5a54b672c0..3463b90b37698a40d623dade70f692a752e19adb 100755 GIT binary patch delta 43267 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 k*7~A3yNG_N0zY~H0>n~s>pFr~m)} delta 5661 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 From ea88bc2b949746e60f1d7e70d312214a2d40a49a Mon Sep 17 00:00:00 2001 From: zamelane Date: Wed, 27 Nov 2024 18:19:51 +0700 Subject: [PATCH 02/12] =?UTF-8?q?=D0=94=D0=BE=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D1=83?= =?UTF-8?q?=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=BE=D0=B1=20=D0=BE=D1=86=D0=B5=D0=BD=D0=BA=D0=B0?= =?UTF-8?q?=D1=85=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=81=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/src/helpers/notificationController.ts | 24 -- apps/api/src/helpers/workerDetector.ts | 5 + apps/api/src/models/Mark/actions/get/index.ts | 1 + .../models/Mark/actions/get/markGetByData.ts | 10 + apps/api/src/models/Mark/actions/index.ts | 1 + .../models/Mark/actions/save/markSaveOrGet.ts | 2 +- .../MarkValue/actions/get/markValueGetById.ts | 7 + .../src/models/Required/actions/get/index.ts | 1 + .../Required/actions/get/requiredGetByData.ts | 8 + apps/api/src/models/Required/actions/index.ts | 1 + .../src/models/Schedule/actions/get/index.ts | 1 + .../actions/get/scheduleGetFromDBById.ts | 7 + .../src/models/Subject/actions/get/index.ts | 1 + .../actions/get/subjectGetFromDBById.ts | 7 + apps/api/src/models/Subject/actions/index.ts | 1 + apps/api/src/models/Task/actions/get/index.ts | 1 + .../models/Task/actions/get/taskGetFromDB.ts | 7 + apps/api/src/models/Task/actions/index.ts | 3 +- .../Task/actions/save/tasksSaveOrGet.ts | 34 +-- apps/api/src/telegramBot.ts | 6 - .../worker/notificator/bot/botLogic/index.ts | 2 + .../notificator/bot/botLogic/registerLogic.ts | 81 ++++++ .../notificator/bot/botLogic/telegramBot.ts | 12 + apps/api/src/worker/notificator/bot/index.ts | 2 + .../helpers/getDetailedInfo.ts | 46 ++++ .../helpers/getSubscribeInfo.ts | 18 ++ .../bot/notificationLogic/helpers/index.ts | 0 .../bot/notificationLogic/index.ts | 2 + .../notificationController.ts | 13 + .../bot/notificationLogic/sendMarkEvent.ts | 24 ++ apps/api/src/worker/notificator/config.ts | 2 +- apps/api/src/worker/notificator/index.ts | 12 - .../messages/helpers/getDayInfo.ts | 15 ++ .../messages/helpers/getSmileByMarkValue.ts | 12 + .../messages/helpers/getWeekDay.ts | 3 + .../notificator/messages/helpers/index.ts | 3 + .../src/worker/notificator/messages/index.ts | 234 ++++++------------ .../messages/templates/addMark/index.ts | 15 ++ .../messages/templates/deleteMark/index.ts | 15 ++ .../messages/templates/updateMark/index.ts | 20 ++ .../notificator/types/EventDetailedInfo.ts | 17 ++ .../src/worker/notificator/types/MarkEvent.ts | 10 + .../worker/notificator/types/SubscribeInfo.ts | 3 + bun.lockb | Bin 206720 -> 206720 bytes 44 files changed, 478 insertions(+), 211 deletions(-) delete mode 100644 apps/api/src/helpers/notificationController.ts create mode 100644 apps/api/src/helpers/workerDetector.ts create mode 100644 apps/api/src/models/Mark/actions/get/index.ts create mode 100644 apps/api/src/models/Mark/actions/get/markGetByData.ts create mode 100644 apps/api/src/models/MarkValue/actions/get/markValueGetById.ts create mode 100644 apps/api/src/models/Required/actions/get/index.ts create mode 100644 apps/api/src/models/Required/actions/get/requiredGetByData.ts create mode 100644 apps/api/src/models/Schedule/actions/get/scheduleGetFromDBById.ts create mode 100644 apps/api/src/models/Subject/actions/get/index.ts create mode 100644 apps/api/src/models/Subject/actions/get/subjectGetFromDBById.ts create mode 100644 apps/api/src/models/Task/actions/get/index.ts create mode 100644 apps/api/src/models/Task/actions/get/taskGetFromDB.ts delete mode 100644 apps/api/src/telegramBot.ts create mode 100644 apps/api/src/worker/notificator/bot/botLogic/index.ts create mode 100644 apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts create mode 100644 apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts create mode 100644 apps/api/src/worker/notificator/bot/index.ts create mode 100644 apps/api/src/worker/notificator/bot/notificationLogic/helpers/getDetailedInfo.ts create mode 100644 apps/api/src/worker/notificator/bot/notificationLogic/helpers/getSubscribeInfo.ts create mode 100644 apps/api/src/worker/notificator/bot/notificationLogic/helpers/index.ts create mode 100644 apps/api/src/worker/notificator/bot/notificationLogic/index.ts create mode 100644 apps/api/src/worker/notificator/bot/notificationLogic/notificationController.ts create mode 100644 apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts create mode 100644 apps/api/src/worker/notificator/messages/helpers/getDayInfo.ts create mode 100644 apps/api/src/worker/notificator/messages/helpers/getSmileByMarkValue.ts create mode 100644 apps/api/src/worker/notificator/messages/helpers/getWeekDay.ts create mode 100644 apps/api/src/worker/notificator/messages/helpers/index.ts create mode 100644 apps/api/src/worker/notificator/messages/templates/addMark/index.ts create mode 100644 apps/api/src/worker/notificator/messages/templates/deleteMark/index.ts create mode 100644 apps/api/src/worker/notificator/messages/templates/updateMark/index.ts create mode 100644 apps/api/src/worker/notificator/types/EventDetailedInfo.ts create mode 100644 apps/api/src/worker/notificator/types/MarkEvent.ts create mode 100644 apps/api/src/worker/notificator/types/SubscribeInfo.ts diff --git a/apps/api/src/helpers/notificationController.ts b/apps/api/src/helpers/notificationController.ts deleted file mode 100644 index 0a708fe4..00000000 --- a/apps/api/src/helpers/notificationController.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { MarkKeys, Task } from '@diary-spo/shared' - -export interface IMarkEvent { - diaryUserId: bigint - task: Task - mark: MarkKeys - previousMarkId: number | null - status: 'ADD' | 'DELETE' | 'UPDATE' - eventDatetime: Date -} - -const ids_marks: IMarkEvent[] = [] // Идентификаторы оценок - -/** - * Регистрирует событие взаимодействия с оценкой - * @param event - */ -export const addNewMarkEvent = (event: IMarkEvent): void => { - ids_marks.push(event) - console.log(event) -} - -export const getMarkEvent = () => - ids_marks.length > 0 ? ids_marks.splice(0, 1)[0] : null diff --git a/apps/api/src/helpers/workerDetector.ts b/apps/api/src/helpers/workerDetector.ts new file mode 100644 index 00000000..24c303bd --- /dev/null +++ b/apps/api/src/helpers/workerDetector.ts @@ -0,0 +1,5 @@ +let isWorker: boolean = false; + +export const setIsWorker = (value: boolean) => isWorker = value; + +export const getIsWorker = () => isWorker; \ No newline at end of file diff --git a/apps/api/src/models/Mark/actions/get/index.ts b/apps/api/src/models/Mark/actions/get/index.ts new file mode 100644 index 00000000..6b29b4d2 --- /dev/null +++ b/apps/api/src/models/Mark/actions/get/index.ts @@ -0,0 +1 @@ +export * from './markGetByData' \ No newline at end of file diff --git a/apps/api/src/models/Mark/actions/get/markGetByData.ts b/apps/api/src/models/Mark/actions/get/markGetByData.ts new file mode 100644 index 00000000..2ce059ea --- /dev/null +++ b/apps/api/src/models/Mark/actions/get/markGetByData.ts @@ -0,0 +1,10 @@ +import type {ICacheData} from "@helpers"; +import {MarkModel} from "../../model"; + +export const markGetByData = async (taskId: bigint, authData: ICacheData) => + MarkModel.findOne({ + where: { + taskId, + diaryUserId: authData.localUserId + } + }) diff --git a/apps/api/src/models/Mark/actions/index.ts b/apps/api/src/models/Mark/actions/index.ts index 44a94cc6..2a5ba90d 100755 --- a/apps/api/src/models/Mark/actions/index.ts +++ b/apps/api/src/models/Mark/actions/index.ts @@ -1,2 +1,3 @@ export * from './save' export * from './delete' +export * from './get' \ No newline at end of file diff --git a/apps/api/src/models/Mark/actions/save/markSaveOrGet.ts b/apps/api/src/models/Mark/actions/save/markSaveOrGet.ts index dfdb759a..46da22ac 100755 --- a/apps/api/src/models/Mark/actions/save/markSaveOrGet.ts +++ b/apps/api/src/models/Mark/actions/save/markSaveOrGet.ts @@ -1,7 +1,7 @@ import type { MarkKeys, Task } from '@diary-spo/shared' import { type ICacheData, retriesForError } from '@helpers' import { formatDate } from '@utils' -import { addNewMarkEvent } from '../../../../helpers/notificationController' +import { addNewMarkEvent } from '../../../../worker/notificator/bot' import { markValueSaveOrGet } from '../../../MarkValue' import type { IScheduleModel } from '../../../Schedule' import { markSaveOrGetUnSafe } from './markSaveOrGetUnSafe' diff --git a/apps/api/src/models/MarkValue/actions/get/markValueGetById.ts b/apps/api/src/models/MarkValue/actions/get/markValueGetById.ts new file mode 100644 index 00000000..7cc4af92 --- /dev/null +++ b/apps/api/src/models/MarkValue/actions/get/markValueGetById.ts @@ -0,0 +1,7 @@ +import {MarkValueModel} from "../../model"; + +export const markValueGetById = async (markValueId: number) => MarkValueModel.findOne({ + where: { + id: markValueId + } +}) \ No newline at end of file diff --git a/apps/api/src/models/Required/actions/get/index.ts b/apps/api/src/models/Required/actions/get/index.ts new file mode 100644 index 00000000..785aed35 --- /dev/null +++ b/apps/api/src/models/Required/actions/get/index.ts @@ -0,0 +1 @@ +export * from './requiredGetByData' \ No newline at end of file diff --git a/apps/api/src/models/Required/actions/get/requiredGetByData.ts b/apps/api/src/models/Required/actions/get/requiredGetByData.ts new file mode 100644 index 00000000..f5253981 --- /dev/null +++ b/apps/api/src/models/Required/actions/get/requiredGetByData.ts @@ -0,0 +1,8 @@ +import {RequiredModel} from "../../model"; + +export const requiredGetByData = (diaryUserId: bigint, taskId: bigint) => RequiredModel.findOne({ + where: { + diaryUserId, + taskId + } +}) \ No newline at end of file diff --git a/apps/api/src/models/Required/actions/index.ts b/apps/api/src/models/Required/actions/index.ts index 6bbfcccf..a7a65665 100755 --- a/apps/api/src/models/Required/actions/index.ts +++ b/apps/api/src/models/Required/actions/index.ts @@ -1 +1,2 @@ export * from './save' +export * from './get' \ No newline at end of file diff --git a/apps/api/src/models/Schedule/actions/get/index.ts b/apps/api/src/models/Schedule/actions/get/index.ts index cb05e97a..04ab9d35 100644 --- a/apps/api/src/models/Schedule/actions/get/index.ts +++ b/apps/api/src/models/Schedule/actions/get/index.ts @@ -1 +1,2 @@ export * from './scheduleGetFromDB' +export * from './scheduleGetFromDBById' \ No newline at end of file diff --git a/apps/api/src/models/Schedule/actions/get/scheduleGetFromDBById.ts b/apps/api/src/models/Schedule/actions/get/scheduleGetFromDBById.ts new file mode 100644 index 00000000..7b6f62ab --- /dev/null +++ b/apps/api/src/models/Schedule/actions/get/scheduleGetFromDBById.ts @@ -0,0 +1,7 @@ +import {ScheduleModel} from "../../model"; + +export const scheduleGetFromDBById = async (scheduleId: bigint) => ScheduleModel.findOne({ + where: { + id: scheduleId + } +}) \ No newline at end of file diff --git a/apps/api/src/models/Subject/actions/get/index.ts b/apps/api/src/models/Subject/actions/get/index.ts new file mode 100644 index 00000000..2e707cf8 --- /dev/null +++ b/apps/api/src/models/Subject/actions/get/index.ts @@ -0,0 +1 @@ +export * from './subjectGetFromDBById' \ No newline at end of file diff --git a/apps/api/src/models/Subject/actions/get/subjectGetFromDBById.ts b/apps/api/src/models/Subject/actions/get/subjectGetFromDBById.ts new file mode 100644 index 00000000..0a2a7dd2 --- /dev/null +++ b/apps/api/src/models/Subject/actions/get/subjectGetFromDBById.ts @@ -0,0 +1,7 @@ +import {SubjectModel} from "../../model"; + +export const subjectGetFromDBById = (subjectId: bigint) => SubjectModel.findOne({ + where: { + id: subjectId + } +}) \ No newline at end of file diff --git a/apps/api/src/models/Subject/actions/index.ts b/apps/api/src/models/Subject/actions/index.ts index 6bbfcccf..a7a65665 100755 --- a/apps/api/src/models/Subject/actions/index.ts +++ b/apps/api/src/models/Subject/actions/index.ts @@ -1 +1,2 @@ export * from './save' +export * from './get' \ No newline at end of file diff --git a/apps/api/src/models/Task/actions/get/index.ts b/apps/api/src/models/Task/actions/get/index.ts new file mode 100644 index 00000000..15f419c7 --- /dev/null +++ b/apps/api/src/models/Task/actions/get/index.ts @@ -0,0 +1 @@ +export * from './taskGetFromDB' \ No newline at end of file diff --git a/apps/api/src/models/Task/actions/get/taskGetFromDB.ts b/apps/api/src/models/Task/actions/get/taskGetFromDB.ts new file mode 100644 index 00000000..f1905b4d --- /dev/null +++ b/apps/api/src/models/Task/actions/get/taskGetFromDB.ts @@ -0,0 +1,7 @@ +import {TaskModel} from "../../model"; + +export const taskGetFromDB = async (taskIdFromDiary: number) => TaskModel.findOne({ + where: { + idFromDiary: taskIdFromDiary + } +}) \ No newline at end of file diff --git a/apps/api/src/models/Task/actions/index.ts b/apps/api/src/models/Task/actions/index.ts index 3a1b4c6b..0b42522c 100755 --- a/apps/api/src/models/Task/actions/index.ts +++ b/apps/api/src/models/Task/actions/index.ts @@ -1,2 +1,3 @@ export * from './delete' -export * from './save' +export * from './get' +export * from './save' \ No newline at end of file diff --git a/apps/api/src/models/Task/actions/save/tasksSaveOrGet.ts b/apps/api/src/models/Task/actions/save/tasksSaveOrGet.ts index de42e9c7..f1bd14b3 100755 --- a/apps/api/src/models/Task/actions/save/tasksSaveOrGet.ts +++ b/apps/api/src/models/Task/actions/save/tasksSaveOrGet.ts @@ -1,15 +1,16 @@ -import type { Task } from '@diary-spo/shared' +import {Task} from '@diary-spo/shared' import { type ICacheData, retriesForError } from '@helpers' import { objPropertyCopy } from 'src/helpers/objPropertyCopy' import { taskTypeSaveOrGet } from 'src/models/TaskType' -import { addNewMarkEvent } from '../../../../helpers/notificationController' -import { markDelete, markSaveOrGet } from '../../../Mark' +import { addNewMarkEvent } from '../../../../worker/notificator/bot' +import {markDelete, markGetByData, markSaveOrGet} from '../../../Mark' import { requiredSaveOrGet } from '../../../Required' import type { IScheduleModel } from '../../../Schedule' import type { ITermDetectP } from '../../../Term' import { TaskModel } from '../../model' import { deleteTasks } from '../delete' +import {markValueGetById} from "../../../MarkValue/actions/get/markValueGetById"; export const tasksSaveOrGet = async ( tasks: Task[], @@ -92,18 +93,23 @@ export const tasksSaveOrGet = async ( systemInitiator ) } else { - markDelete(taskId, authData).then((count) => { + let deletedMarkRaw = await markGetByData(taskId, authData) + + markDelete(taskId, authData).then(async (count) => { // Игнорируем не системные инициализаторы (т.е. если пользователь уже сам посмотрел) - if (count > 0 && systemInitiator) - // Регистрируем событие удаления оценки - addNewMarkEvent({ - mark: task.mark, - task, - diaryUserId: authData.localUserId, - status: 'DELETE', - eventDatetime: new Date(), - previousMarkId: null - }) + if (count > 0 && systemInitiator && deletedMarkRaw) { + let markValue = await markValueGetById(deletedMarkRaw.markValueId) + if (markValue) + // Регистрируем событие удаления оценки + addNewMarkEvent({ + mark: markValue.value, + task, + diaryUserId: authData.localUserId, + status: 'DELETE', + eventDatetime: new Date(), + previousMarkId: null + }) + } }) } diff --git a/apps/api/src/telegramBot.ts b/apps/api/src/telegramBot.ts deleted file mode 100644 index 9e0365c3..00000000 --- a/apps/api/src/telegramBot.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { BOT_TOKEN } from '@config' -import TelegramBot from 'node-telegram-bot-api' - -const token = BOT_TOKEN - -export const bot = new TelegramBot(token, { polling: true }) diff --git a/apps/api/src/worker/notificator/bot/botLogic/index.ts b/apps/api/src/worker/notificator/bot/botLogic/index.ts new file mode 100644 index 00000000..41f36cbe --- /dev/null +++ b/apps/api/src/worker/notificator/bot/botLogic/index.ts @@ -0,0 +1,2 @@ +export * from './telegramBot' +export * from './registerLogic' \ No newline at end of file diff --git a/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts new file mode 100644 index 00000000..de381fac --- /dev/null +++ b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts @@ -0,0 +1,81 @@ +import {getCookieFromToken} from "@helpers"; +import {SubscribeModel} from "../../../../models/Subscribe"; +import {DiaryUserModel} from "../../../../models/DiaryUser"; +import TelegramBot from "node-telegram-bot-api"; + +export const registerLogic = (bot: TelegramBot | null) => { + if (!bot) + return + bot.on('message', async (msg) => { + const chatId = msg.chat.id + + if (!msg.text) bot.sendMessage(chatId, 'Такое сообщение не поддерживается') + + const command = (msg.text ?? '').split(' ') + + switch (command[0]) { + case '/subscribe': { + if (command.length < 2) { + bot.sendMessage( + chatId, + 'Передайте вторым параметром актуальный токен, чтобы подписаться на уведомления' + ) + return + } + const token = command[1] + + const auth = await getCookieFromToken(token).catch(() => null) + + if (!auth) { + bot.sendMessage(chatId, 'Токен не найден ...') + return + } + + const subscribes = await SubscribeModel.findAll({ + where: { + tgId: chatId + } + }) + + if (subscribes.length >= 1) { + bot.sendMessage( + chatId, + 'Вы уже подписаны для других аккаунтов. Сначала отпишитесь от остальных (/unsubscribe)' + ) + return + } + + await SubscribeModel.create({ + diaryUserId: auth.localUserId, + tgId: BigInt(chatId) + }) + + const user = await DiaryUserModel.findOne({ + where: { + id: auth.localUserId + } + }) + + bot.sendMessage( + chatId, + `Вы подписались на аккаунт c ФИО => ${user?.firstName} ${user?.lastName} ${user?.middleName}` + ) + break + } + case '/unsubscribe': + await SubscribeModel.destroy({ + where: { + tgId: chatId + } + }) + bot.sendMessage( + chatId, + 'Вы успешно отписались от всех аккаунтов. Можете привязать новый (/subscribe)' + ) + break + default: + bot.sendMessage(chatId, 'Такой команды нету') + break + } + }) +} \ No newline at end of file diff --git a/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts b/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts new file mode 100644 index 00000000..0a5018f8 --- /dev/null +++ b/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts @@ -0,0 +1,12 @@ +import { BOT_TOKEN } from '@config' +import TelegramBot from 'node-telegram-bot-api' +import {registerLogic} from "./registerLogic" + +const token = BOT_TOKEN + +export const bot = Bun.main.includes('main.ts') || token == 'IGNORE' + ? null + : new TelegramBot(token, { polling: true }) + +if (bot) + registerLogic(bot) \ No newline at end of file diff --git a/apps/api/src/worker/notificator/bot/index.ts b/apps/api/src/worker/notificator/bot/index.ts new file mode 100644 index 00000000..43f256b4 --- /dev/null +++ b/apps/api/src/worker/notificator/bot/index.ts @@ -0,0 +1,2 @@ +export * from './botLogic' +export * from './notificationLogic' \ No newline at end of file diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getDetailedInfo.ts b/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getDetailedInfo.ts new file mode 100644 index 00000000..e888d6bc --- /dev/null +++ b/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getDetailedInfo.ts @@ -0,0 +1,46 @@ +import {taskGetFromDB} from "../../../../../models/Task"; +import {MarkEvent} from "../../../types/MarkEvent"; +import {scheduleGetFromDBById} from "../../../../../models/Schedule"; +import {subjectGetFromDBById} from "../../../../../models/Subject"; +import {markValueGetById} from "../../../../../models/MarkValue/actions/get/markValueGetById"; +import {EventDetailedInfo} from "../../../types/EventDetailedInfo"; +import {requiredGetByData} from "../../../../../models/Required"; +import {Grade} from "@diary-spo/shared"; + +export const getDetailedInfo = async (event: MarkEvent): Promise => { + const task = await taskGetFromDB(event.task.id) + + if (!task) + return null + + const taskRequired = await requiredGetByData(event.diaryUserId, task.id) + + if (!taskRequired) + return null + + const schedule = await scheduleGetFromDBById(task.scheduleId) + + if (!schedule || !schedule?.subjectId) + return null + + const subject = await subjectGetFromDBById(schedule.subjectId) + + if (!subject) + return null + + const previousMark = event.previousMarkId + ? (await markValueGetById(event.previousMarkId))?.value ?? null + : null + + const isCredit = event.mark === 'Two' && taskRequired.isRequired + + return { + task, + subject, + schedule, + previousMark: previousMark ? Grade[previousMark] : null, + currentMark: isCredit ? 'Д' : Grade[event.mark], + eventDatetime: event.eventDatetime, + status: event.status + } +} \ No newline at end of file diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getSubscribeInfo.ts b/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getSubscribeInfo.ts new file mode 100644 index 00000000..092f44b9 --- /dev/null +++ b/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getSubscribeInfo.ts @@ -0,0 +1,18 @@ +import {MarkEvent} from "../../../types/MarkEvent"; +import {SubscribeModel} from "../../../../../models/Subscribe"; +import {SubscribeInfo} from "../../../types/SubscribeInfo"; + +export const getSubscribeInfo = async (event: MarkEvent): Promise => { + const subscribe = await SubscribeModel.findOne({ + where: { + diaryUserId: event.diaryUserId + } + }) + + if (!subscribe) + return null + + return { + tgId: subscribe.tgId + } +} \ No newline at end of file diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/helpers/index.ts b/apps/api/src/worker/notificator/bot/notificationLogic/helpers/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/index.ts b/apps/api/src/worker/notificator/bot/notificationLogic/index.ts new file mode 100644 index 00000000..33c0a859 --- /dev/null +++ b/apps/api/src/worker/notificator/bot/notificationLogic/index.ts @@ -0,0 +1,2 @@ +export * from './sendMarkEvent' +export * from './notificationController' \ No newline at end of file diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/notificationController.ts b/apps/api/src/worker/notificator/bot/notificationLogic/notificationController.ts new file mode 100644 index 00000000..6412a070 --- /dev/null +++ b/apps/api/src/worker/notificator/bot/notificationLogic/notificationController.ts @@ -0,0 +1,13 @@ +import {MarkEvent} from "../../types/MarkEvent"; +import {sendMarkEvent} from "./sendMarkEvent"; + +/** + * Регистрирует событие взаимодействия с оценкой + * @param event + */ +export const addNewMarkEvent = async (event: MarkEvent): Promise => { + await sendMarkEvent(event); +} + +// comment: все события описываются здесь +// напиример, пока что тут только событие на оценки (но в будущем будет и на расписание) \ No newline at end of file diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts b/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts new file mode 100644 index 00000000..bb8a84fa --- /dev/null +++ b/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts @@ -0,0 +1,24 @@ +import {MarkEvent} from "../../types/MarkEvent"; +import {getSubscribeInfo} from "./helpers/getSubscribeInfo"; +import {getDetailedInfo} from "./helpers/getDetailedInfo"; +import {buildMessageByEvent} from "../../messages"; +import {bot} from "../botLogic"; + +export const sendMarkEvent = async (event: MarkEvent) => { + const subscribe = await getSubscribeInfo(event) + + if (!subscribe) + return + + const detailedInfo = await getDetailedInfo(event) + + if (!detailedInfo) + return + + const message = buildMessageByEvent(detailedInfo) + + if (!bot || !message) + return + + bot.sendMessage(String(subscribe.tgId), message) +} \ No newline at end of file diff --git a/apps/api/src/worker/notificator/config.ts b/apps/api/src/worker/notificator/config.ts index 5effb64c..d7a73b23 100644 --- a/apps/api/src/worker/notificator/config.ts +++ b/apps/api/src/worker/notificator/config.ts @@ -1,2 +1,2 @@ // Интервал повторения перепроверки новых оценок -export const INTERVAL_RUN = 2 * 60 // каждые 2 минуты +export const INTERVAL_RUN = 2 * 60 // каждые 2 минуты \ No newline at end of file diff --git a/apps/api/src/worker/notificator/index.ts b/apps/api/src/worker/notificator/index.ts index 7fbdb9c8..566d38ac 100644 --- a/apps/api/src/worker/notificator/index.ts +++ b/apps/api/src/worker/notificator/index.ts @@ -1,13 +1,10 @@ import { getCookieFromToken } from '@helpers' -import { getMarkEvent } from '../../helpers/notificationController' import { AuthModel } from '../../models/Auth' -import { DiaryUserModel } from '../../models/DiaryUser' import { SubscribeModel } from '../../models/Subscribe' import { getCurrPerformance } from '../../routes/performance.current/service' import { checkInterval } from '../utils/checkInterval' import { logger } from '../utils/logger' import { INTERVAL_RUN } from './config' -import { sendEvent } from './messages' let lastRunning: Date | null = null const log = logger('notificator') @@ -37,13 +34,4 @@ export const notificatorChecker = async (): Promise => { await getCurrPerformance(cacheData, true) } - - let markEvent = getMarkEvent() - console.log(markEvent) - - while (markEvent) { - await sendEvent(markEvent) - // Запрашиваем следующие событие - markEvent = getMarkEvent() - } } diff --git a/apps/api/src/worker/notificator/messages/helpers/getDayInfo.ts b/apps/api/src/worker/notificator/messages/helpers/getDayInfo.ts new file mode 100644 index 00000000..8ebe4ed9 --- /dev/null +++ b/apps/api/src/worker/notificator/messages/helpers/getDayInfo.ts @@ -0,0 +1,15 @@ +import {getWeekDay} from "./getWeekDay"; +import {EventDetailedInfo} from "../../types/EventDetailedInfo"; + +export const getDayInfo = (info: EventDetailedInfo) => { + const date = new Date(info.schedule.date) + const dayName = getWeekDay(date) + const day = `0${date.getUTCDate()}`.slice(-2) + const month = `0${date.getUTCMonth() + 1}`.slice(-2) + + return { + dayName, + day, + month + } +} \ No newline at end of file diff --git a/apps/api/src/worker/notificator/messages/helpers/getSmileByMarkValue.ts b/apps/api/src/worker/notificator/messages/helpers/getSmileByMarkValue.ts new file mode 100644 index 00000000..d731d92e --- /dev/null +++ b/apps/api/src/worker/notificator/messages/helpers/getSmileByMarkValue.ts @@ -0,0 +1,12 @@ +import {AdditionalMarks} from "@diary-spo/shared"; + +const emojis: Record = { + 5: '5️⃣', + 4: '4️⃣', + 3: '3️⃣', + 2: '2️⃣', + 'Зч': 'Зч (🥳)', + 'Д': 'Д (🫡)' +} + +export const getSmileByMarkValue = (mark: AdditionalMarks | number) => emojis[mark] \ No newline at end of file diff --git a/apps/api/src/worker/notificator/messages/helpers/getWeekDay.ts b/apps/api/src/worker/notificator/messages/helpers/getWeekDay.ts new file mode 100644 index 00000000..ee26d166 --- /dev/null +++ b/apps/api/src/worker/notificator/messages/helpers/getWeekDay.ts @@ -0,0 +1,3 @@ +const days = ['вс', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб'] + +export const getWeekDay = (date: Date) => days[date.getDay()] \ No newline at end of file diff --git a/apps/api/src/worker/notificator/messages/helpers/index.ts b/apps/api/src/worker/notificator/messages/helpers/index.ts new file mode 100644 index 00000000..3619b108 --- /dev/null +++ b/apps/api/src/worker/notificator/messages/helpers/index.ts @@ -0,0 +1,3 @@ +export * from './getWeekDay' +export * from './getDayInfo' +export * from './getSmileByMarkValue' \ No newline at end of file diff --git a/apps/api/src/worker/notificator/messages/index.ts b/apps/api/src/worker/notificator/messages/index.ts index 32c2cc5f..1dcfdf18 100644 --- a/apps/api/src/worker/notificator/messages/index.ts +++ b/apps/api/src/worker/notificator/messages/index.ts @@ -1,153 +1,83 @@ -import { Grade } from '@diary-spo/shared' -import { getCookieFromToken } from '@helpers' -import { bot } from 'src/telegramBot' -import type { IMarkEvent } from '../../../helpers/notificationController' -import { DiaryUserModel } from '../../../models/DiaryUser' -import { MarkModel } from '../../../models/Mark' -import { MarkValueModel } from '../../../models/MarkValue' -import { ScheduleModel } from '../../../models/Schedule' -import { SubjectModel } from '../../../models/Subject' -import { SubscribeModel } from '../../../models/Subscribe' -import { TaskModel } from '../../../models/Task' - -bot.on('message', async (msg) => { - const chatId = msg.chat.id - - if (!msg.text) bot.sendMessage(chatId, 'Такое сообщение не поддерживается') - - const command = (msg.text ?? '').split(' ') - - switch (command[0]) { - case '/subscribe': { - if (command.length < 2) { - bot.sendMessage( - chatId, - 'Передайте вторым параметром актуальный токен, чтобы подписаться на уведомления' - ) - return - } - const token = command[1] - - const auth = await getCookieFromToken(token).catch(() => null) - - if (!auth) { - bot.sendMessage(chatId, 'Токен не найден ...') - return - } - - const subscribes = await SubscribeModel.findAll({ - where: { - tgId: chatId - } - }) - - if (subscribes.length >= 1) { - bot.sendMessage( - chatId, - 'Вы уже подписаны для других аккаунтов. Сначала отпишитесь от остальных (/unsubscribe)' - ) - return - } - - await SubscribeModel.create({ - diaryUserId: auth.localUserId, - tgId: BigInt(chatId) - }) - - const user = await DiaryUserModel.findOne({ - where: { - id: auth.localUserId - } - }) - - bot.sendMessage( - chatId, - `Вы подписались на аккаунт c ФИО => ${user?.firstName} ${user?.lastName} ${user?.middleName}` - ) - break - } - case '/unsubscribe': - await SubscribeModel.destroy({ - where: { - tgId: chatId - } - }) - bot.sendMessage( - chatId, - 'Вы успешно отписались от всех аккаунтов. Можете привязать новый (/subscribe)' - ) - break - default: - bot.sendMessage(chatId, 'Такой команды нету') - break - } -}) - -export const sendEvent = async (event: IMarkEvent) => { - const subscribe = await SubscribeModel.findOne({ - where: { - diaryUserId: event.diaryUserId - } - }) - - if (!subscribe) return - - let message = '' - - switch (event.status) { - case 'ADD': - message += 'НОВАЯ ОЦЕНКА!' - break - case 'UPDATE': - message += 'ВАМ ИЗМЕНИЛИ ОЦЕНКУ!' - break - case 'DELETE': - message += 'ВАМ УБРАЛИ ОЦЕНКУ!' - break - } - - const task = await TaskModel.findOne({ - where: { - idFromDiary: event.task.id - } - }) - - if (!task) return - - const schedule = await ScheduleModel.findOne({ - where: { - id: task.scheduleId - } - }) - - if (!schedule || !schedule?.subjectId) return - - const subject = await SubjectModel.findOne({ - where: { - id: schedule.subjectId - } - }) - - if (!subject) return - - const previousMark = event.previousMarkId - ? await MarkValueModel.findOne({ - where: { - id: event.previousMarkId - } - }) - : null - - message += ` - Оценка: ${Grade[event.mark] + (previousMark ? ` (предыдущая ${Grade[previousMark.value]})` : '')}, - Предмет: ${subject.name} (дата: ${schedule.date}) - - Задание - Тема: ${event.task.topic ?? '[НЕ ЗАДАНО]'} - Описание: ${event.task.condition ?? '[НЕ ЗАДАНО]'} - - Время обнаружения: ${event.eventDatetime.toUTCString()} - ` - - bot.sendMessage(String(subscribe.tgId), message) +import {EventDetailedInfo} from "../types/EventDetailedInfo"; +import {buildAddMarkMessage} from "./templates/addMark"; +import {buildDeleteMarkMessage} from "./templates/deleteMark"; +import {buildUpdateMarkMessage} from "./templates/updateMark"; + +const messages = { + 'ADD': buildAddMarkMessage, + 'DELETE': buildDeleteMarkMessage, + 'UPDATE': buildUpdateMarkMessage } + +export const buildMessageByEvent = (info: EventDetailedInfo) => messages[info.status](info) + +export * from './helpers' + +// export const sendMarkEvent = async (event: MarkEvent) => { +// const subscribe = await SubscribeModel.findOne({ +// where: { +// diaryUserId: event.diaryUserId +// } +// }) +// +// if (!subscribe) return +// +// let message = '' +// +// switch (event.status) { +// case 'ADD': +// message += 'НОВАЯ ОЦЕНКА!' +// break +// case 'UPDATE': +// message += 'ВАМ ИЗМЕНИЛИ ОЦЕНКУ!' +// break +// case 'DELETE': +// message += 'ВАМ УБРАЛИ ОЦЕНКУ!' +// break +// } +// +// const task = await TaskModel.findOne({ +// where: { +// idFromDiary: event.task.id +// } +// }) +// +// if (!task) return +// +// const schedule = await ScheduleModel.findOne({ +// where: { +// id: task.scheduleId +// } +// }) +// +// if (!schedule || !schedule?.subjectId) return +// +// const subject = await SubjectModel.findOne({ +// where: { +// id: schedule.subjectId +// } +// }) +// +// if (!subject) return +// +// const previousMark = event.previousMarkId +// ? await MarkValueModel.findOne({ +// where: { +// id: event.previousMarkId +// } +// }) +// : null +// +// message += ` +// Оценка: ${Grade[event.mark] + (previousMark ? ` (предыдущая ${Grade[previousMark.value]})` : '')}, +// Предмет: ${subject.name} (дата: ${schedule.date}) +// +// Задание +// Тема: ${event.task.topic ?? '[НЕ ЗАДАНО]'} +// Описание: ${event.task.condition ?? '[НЕ ЗАДАНО]'} +// +// Время обнаружения: ${event.eventDatetime.toUTCString()} +// ` +// +// bot.sendMessage(String(subscribe.tgId), message) +// } diff --git a/apps/api/src/worker/notificator/messages/templates/addMark/index.ts b/apps/api/src/worker/notificator/messages/templates/addMark/index.ts new file mode 100644 index 00000000..c5e5e9f8 --- /dev/null +++ b/apps/api/src/worker/notificator/messages/templates/addMark/index.ts @@ -0,0 +1,15 @@ +import {EventDetailedInfo} from "../../../types/EventDetailedInfo"; +import {getDayInfo} from "../../helpers"; +import {getSmileByMarkValue} from "../../helpers"; + +export const buildAddMarkMessage = (info: EventDetailedInfo) => { + const markValue = info.currentMark + + const dayInfo = getDayInfo(info) + + const subjectName = info.subject.name + + const emoji = getSmileByMarkValue(markValue) + + return `${emoji} на ${dayInfo.day}.${dayInfo.month} (${dayInfo.dayName}) по ${subjectName}` +} \ No newline at end of file diff --git a/apps/api/src/worker/notificator/messages/templates/deleteMark/index.ts b/apps/api/src/worker/notificator/messages/templates/deleteMark/index.ts new file mode 100644 index 00000000..5954a1e2 --- /dev/null +++ b/apps/api/src/worker/notificator/messages/templates/deleteMark/index.ts @@ -0,0 +1,15 @@ +import {EventDetailedInfo} from "../../../types/EventDetailedInfo"; +import {getDayInfo} from "../../helpers"; +import {getSmileByMarkValue} from "../../helpers"; + +export const buildDeleteMarkMessage = (info: EventDetailedInfo) => { + const markValue = info.currentMark + + const dayInfo = getDayInfo(info) + + const subjectName = info.subject.name + + const emoji = getSmileByMarkValue(markValue) + + return `Убрана ${emoji} на ${dayInfo.day}.${dayInfo.month} (${dayInfo.dayName}) по ${subjectName}` +} \ No newline at end of file diff --git a/apps/api/src/worker/notificator/messages/templates/updateMark/index.ts b/apps/api/src/worker/notificator/messages/templates/updateMark/index.ts new file mode 100644 index 00000000..e3ccbbf2 --- /dev/null +++ b/apps/api/src/worker/notificator/messages/templates/updateMark/index.ts @@ -0,0 +1,20 @@ +import {EventDetailedInfo} from "../../../types/EventDetailedInfo"; +import {getDayInfo} from "../../helpers"; +import {getSmileByMarkValue} from "../../helpers"; + +export const buildUpdateMarkMessage = (info: EventDetailedInfo) => { + const markValue = info.currentMark + const previousMarkValue = info.previousMark + + if (!previousMarkValue) + return null + + const dayInfo = getDayInfo(info) + + const subjectName = info.subject.name + + const emoji = getSmileByMarkValue(markValue) + const previousEmoji = getSmileByMarkValue(previousMarkValue) + + return `${emoji} (было ${previousEmoji}) на ${dayInfo.day}.${dayInfo.month} (${dayInfo.dayName}) по ${subjectName}` +} \ No newline at end of file diff --git a/apps/api/src/worker/notificator/types/EventDetailedInfo.ts b/apps/api/src/worker/notificator/types/EventDetailedInfo.ts new file mode 100644 index 00000000..042dda99 --- /dev/null +++ b/apps/api/src/worker/notificator/types/EventDetailedInfo.ts @@ -0,0 +1,17 @@ +import {IScheduleModel} from "../../../models/Schedule"; +import {ITaskModel} from "../../../models/Task"; +import {ISubjectModelType} from "../../../models/Subject"; +import {type AdditionalMarks} from "@diary-spo/shared"; + +export interface EventDetailedInfo { + // Информация об источнике события + subject: ISubjectModelType + schedule: IScheduleModel + task: ITaskModel + // Информация об изменениях + previousMark: AdditionalMarks | number | null + currentMark: AdditionalMarks | number + status: "ADD" | "DELETE" | "UPDATE" + // Доп. инфа + eventDatetime: Date +} \ No newline at end of file diff --git a/apps/api/src/worker/notificator/types/MarkEvent.ts b/apps/api/src/worker/notificator/types/MarkEvent.ts new file mode 100644 index 00000000..43f13df4 --- /dev/null +++ b/apps/api/src/worker/notificator/types/MarkEvent.ts @@ -0,0 +1,10 @@ +import {MarkKeys, Task} from "@diary-spo/shared"; + +export interface MarkEvent { + diaryUserId: bigint + task: Task + mark: MarkKeys + previousMarkId: number | null + status: 'ADD' | 'DELETE' | 'UPDATE' + eventDatetime: Date +} \ No newline at end of file diff --git a/apps/api/src/worker/notificator/types/SubscribeInfo.ts b/apps/api/src/worker/notificator/types/SubscribeInfo.ts new file mode 100644 index 00000000..93f80fb9 --- /dev/null +++ b/apps/api/src/worker/notificator/types/SubscribeInfo.ts @@ -0,0 +1,3 @@ +export interface SubscribeInfo { + tgId: bigint +} \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 3463b90b37698a40d623dade70f692a752e19adb..b570d85981b8b2706f25ff4b2c5cc892e07d4b18 100755 GIT binary patch delta 28 icmZoT&(i=zEsR^3gu^%(;|%o7O!UmRONTL~UIYM$VF=Fv delta 28 dcmZoT&(i=zEsR^3gu^(P7$9J~bQn|WMF4n?2Soq? From dca8854cc99d1f4712b92a99765bbd93ad9ce6b0 Mon Sep 17 00:00:00 2001 From: zamelane Date: Wed, 27 Nov 2024 18:28:24 +0700 Subject: [PATCH 03/12] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D1=81=D0=BA=D0=BE=D0=B1=D0=BA=D0=B8=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=8D=D0=BC=D0=BE=D0=B4=D0=B7=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/helpers/workerDetector.ts | 6 +- apps/api/src/models/Mark/actions/get/index.ts | 2 +- .../models/Mark/actions/get/markGetByData.ts | 16 +- apps/api/src/models/Mark/actions/index.ts | 2 +- .../MarkValue/actions/get/markValueGetById.ts | 9 +- .../src/models/Required/actions/get/index.ts | 2 +- .../Required/actions/get/requiredGetByData.ts | 11 +- apps/api/src/models/Required/actions/index.ts | 2 +- .../src/models/Schedule/actions/get/index.ts | 2 +- .../actions/get/scheduleGetFromDBById.ts | 9 +- .../src/models/Subject/actions/get/index.ts | 2 +- .../actions/get/subjectGetFromDBById.ts | 9 +- apps/api/src/models/Subject/actions/index.ts | 2 +- apps/api/src/models/Task/actions/get/index.ts | 2 +- .../models/Task/actions/get/taskGetFromDB.ts | 9 +- apps/api/src/models/Task/actions/index.ts | 2 +- .../Task/actions/save/tasksSaveOrGet.ts | 14 +- .../worker/notificator/bot/botLogic/index.ts | 2 +- .../notificator/bot/botLogic/registerLogic.ts | 137 +++++++++--------- .../notificator/bot/botLogic/telegramBot.ts | 8 +- apps/api/src/worker/notificator/bot/index.ts | 2 +- .../helpers/getDetailedInfo.ts | 68 +++++---- .../helpers/getSubscribeInfo.ts | 31 ++-- .../bot/notificationLogic/index.ts | 2 +- .../notificationController.ts | 8 +- .../bot/notificationLogic/sendMarkEvent.ts | 29 ++-- apps/api/src/worker/notificator/config.ts | 2 +- .../messages/helpers/getDayInfo.ts | 24 +-- .../messages/helpers/getSmileByMarkValue.ts | 17 ++- .../messages/helpers/getWeekDay.ts | 2 +- .../notificator/messages/helpers/index.ts | 2 +- .../src/worker/notificator/messages/index.ts | 17 ++- .../messages/templates/addMark/index.ts | 18 +-- .../messages/templates/deleteMark/index.ts | 18 +-- .../messages/templates/updateMark/index.ts | 25 ++-- .../notificator/types/EventDetailedInfo.ts | 30 ++-- .../src/worker/notificator/types/MarkEvent.ts | 16 +- .../worker/notificator/types/SubscribeInfo.ts | 4 +- 38 files changed, 283 insertions(+), 280 deletions(-) diff --git a/apps/api/src/helpers/workerDetector.ts b/apps/api/src/helpers/workerDetector.ts index 24c303bd..872be68d 100644 --- a/apps/api/src/helpers/workerDetector.ts +++ b/apps/api/src/helpers/workerDetector.ts @@ -1,5 +1,5 @@ -let isWorker: boolean = false; +let isWorker = false -export const setIsWorker = (value: boolean) => isWorker = value; +export const setIsWorker = (value: boolean) => (isWorker = value) -export const getIsWorker = () => isWorker; \ No newline at end of file +export const getIsWorker = () => isWorker diff --git a/apps/api/src/models/Mark/actions/get/index.ts b/apps/api/src/models/Mark/actions/get/index.ts index 6b29b4d2..957bb3b1 100644 --- a/apps/api/src/models/Mark/actions/get/index.ts +++ b/apps/api/src/models/Mark/actions/get/index.ts @@ -1 +1 @@ -export * from './markGetByData' \ No newline at end of file +export * from './markGetByData' diff --git a/apps/api/src/models/Mark/actions/get/markGetByData.ts b/apps/api/src/models/Mark/actions/get/markGetByData.ts index 2ce059ea..281e626b 100644 --- a/apps/api/src/models/Mark/actions/get/markGetByData.ts +++ b/apps/api/src/models/Mark/actions/get/markGetByData.ts @@ -1,10 +1,10 @@ -import type {ICacheData} from "@helpers"; -import {MarkModel} from "../../model"; +import type { ICacheData } from '@helpers' +import { MarkModel } from '../../model' export const markGetByData = async (taskId: bigint, authData: ICacheData) => - MarkModel.findOne({ - where: { - taskId, - diaryUserId: authData.localUserId - } - }) + MarkModel.findOne({ + where: { + taskId, + diaryUserId: authData.localUserId + } + }) diff --git a/apps/api/src/models/Mark/actions/index.ts b/apps/api/src/models/Mark/actions/index.ts index 2a5ba90d..a5f43695 100755 --- a/apps/api/src/models/Mark/actions/index.ts +++ b/apps/api/src/models/Mark/actions/index.ts @@ -1,3 +1,3 @@ export * from './save' export * from './delete' -export * from './get' \ No newline at end of file +export * from './get' diff --git a/apps/api/src/models/MarkValue/actions/get/markValueGetById.ts b/apps/api/src/models/MarkValue/actions/get/markValueGetById.ts index 7cc4af92..a01d1702 100644 --- a/apps/api/src/models/MarkValue/actions/get/markValueGetById.ts +++ b/apps/api/src/models/MarkValue/actions/get/markValueGetById.ts @@ -1,7 +1,8 @@ -import {MarkValueModel} from "../../model"; +import { MarkValueModel } from '../../model' -export const markValueGetById = async (markValueId: number) => MarkValueModel.findOne({ +export const markValueGetById = async (markValueId: number) => + MarkValueModel.findOne({ where: { - id: markValueId + id: markValueId } -}) \ No newline at end of file + }) diff --git a/apps/api/src/models/Required/actions/get/index.ts b/apps/api/src/models/Required/actions/get/index.ts index 785aed35..00190704 100644 --- a/apps/api/src/models/Required/actions/get/index.ts +++ b/apps/api/src/models/Required/actions/get/index.ts @@ -1 +1 @@ -export * from './requiredGetByData' \ No newline at end of file +export * from './requiredGetByData' diff --git a/apps/api/src/models/Required/actions/get/requiredGetByData.ts b/apps/api/src/models/Required/actions/get/requiredGetByData.ts index f5253981..0566e1d6 100644 --- a/apps/api/src/models/Required/actions/get/requiredGetByData.ts +++ b/apps/api/src/models/Required/actions/get/requiredGetByData.ts @@ -1,8 +1,9 @@ -import {RequiredModel} from "../../model"; +import { RequiredModel } from '../../model' -export const requiredGetByData = (diaryUserId: bigint, taskId: bigint) => RequiredModel.findOne({ +export const requiredGetByData = (diaryUserId: bigint, taskId: bigint) => + RequiredModel.findOne({ where: { - diaryUserId, - taskId + diaryUserId, + taskId } -}) \ No newline at end of file + }) diff --git a/apps/api/src/models/Required/actions/index.ts b/apps/api/src/models/Required/actions/index.ts index a7a65665..1ab59d81 100755 --- a/apps/api/src/models/Required/actions/index.ts +++ b/apps/api/src/models/Required/actions/index.ts @@ -1,2 +1,2 @@ export * from './save' -export * from './get' \ No newline at end of file +export * from './get' diff --git a/apps/api/src/models/Schedule/actions/get/index.ts b/apps/api/src/models/Schedule/actions/get/index.ts index 04ab9d35..738c599b 100644 --- a/apps/api/src/models/Schedule/actions/get/index.ts +++ b/apps/api/src/models/Schedule/actions/get/index.ts @@ -1,2 +1,2 @@ export * from './scheduleGetFromDB' -export * from './scheduleGetFromDBById' \ No newline at end of file +export * from './scheduleGetFromDBById' diff --git a/apps/api/src/models/Schedule/actions/get/scheduleGetFromDBById.ts b/apps/api/src/models/Schedule/actions/get/scheduleGetFromDBById.ts index 7b6f62ab..28bf1133 100644 --- a/apps/api/src/models/Schedule/actions/get/scheduleGetFromDBById.ts +++ b/apps/api/src/models/Schedule/actions/get/scheduleGetFromDBById.ts @@ -1,7 +1,8 @@ -import {ScheduleModel} from "../../model"; +import { ScheduleModel } from '../../model' -export const scheduleGetFromDBById = async (scheduleId: bigint) => ScheduleModel.findOne({ +export const scheduleGetFromDBById = async (scheduleId: bigint) => + ScheduleModel.findOne({ where: { - id: scheduleId + id: scheduleId } -}) \ No newline at end of file + }) diff --git a/apps/api/src/models/Subject/actions/get/index.ts b/apps/api/src/models/Subject/actions/get/index.ts index 2e707cf8..d59668a0 100644 --- a/apps/api/src/models/Subject/actions/get/index.ts +++ b/apps/api/src/models/Subject/actions/get/index.ts @@ -1 +1 @@ -export * from './subjectGetFromDBById' \ No newline at end of file +export * from './subjectGetFromDBById' diff --git a/apps/api/src/models/Subject/actions/get/subjectGetFromDBById.ts b/apps/api/src/models/Subject/actions/get/subjectGetFromDBById.ts index 0a2a7dd2..66824368 100644 --- a/apps/api/src/models/Subject/actions/get/subjectGetFromDBById.ts +++ b/apps/api/src/models/Subject/actions/get/subjectGetFromDBById.ts @@ -1,7 +1,8 @@ -import {SubjectModel} from "../../model"; +import { SubjectModel } from '../../model' -export const subjectGetFromDBById = (subjectId: bigint) => SubjectModel.findOne({ +export const subjectGetFromDBById = (subjectId: bigint) => + SubjectModel.findOne({ where: { - id: subjectId + id: subjectId } -}) \ No newline at end of file + }) diff --git a/apps/api/src/models/Subject/actions/index.ts b/apps/api/src/models/Subject/actions/index.ts index a7a65665..1ab59d81 100755 --- a/apps/api/src/models/Subject/actions/index.ts +++ b/apps/api/src/models/Subject/actions/index.ts @@ -1,2 +1,2 @@ export * from './save' -export * from './get' \ No newline at end of file +export * from './get' diff --git a/apps/api/src/models/Task/actions/get/index.ts b/apps/api/src/models/Task/actions/get/index.ts index 15f419c7..fe59937d 100644 --- a/apps/api/src/models/Task/actions/get/index.ts +++ b/apps/api/src/models/Task/actions/get/index.ts @@ -1 +1 @@ -export * from './taskGetFromDB' \ No newline at end of file +export * from './taskGetFromDB' diff --git a/apps/api/src/models/Task/actions/get/taskGetFromDB.ts b/apps/api/src/models/Task/actions/get/taskGetFromDB.ts index f1905b4d..045fe466 100644 --- a/apps/api/src/models/Task/actions/get/taskGetFromDB.ts +++ b/apps/api/src/models/Task/actions/get/taskGetFromDB.ts @@ -1,7 +1,8 @@ -import {TaskModel} from "../../model"; +import { TaskModel } from '../../model' -export const taskGetFromDB = async (taskIdFromDiary: number) => TaskModel.findOne({ +export const taskGetFromDB = async (taskIdFromDiary: number) => + TaskModel.findOne({ where: { - idFromDiary: taskIdFromDiary + idFromDiary: taskIdFromDiary } -}) \ No newline at end of file + }) diff --git a/apps/api/src/models/Task/actions/index.ts b/apps/api/src/models/Task/actions/index.ts index 0b42522c..f95895df 100755 --- a/apps/api/src/models/Task/actions/index.ts +++ b/apps/api/src/models/Task/actions/index.ts @@ -1,3 +1,3 @@ export * from './delete' export * from './get' -export * from './save' \ No newline at end of file +export * from './save' diff --git a/apps/api/src/models/Task/actions/save/tasksSaveOrGet.ts b/apps/api/src/models/Task/actions/save/tasksSaveOrGet.ts index f1bd14b3..7ac0d7d9 100755 --- a/apps/api/src/models/Task/actions/save/tasksSaveOrGet.ts +++ b/apps/api/src/models/Task/actions/save/tasksSaveOrGet.ts @@ -1,16 +1,16 @@ -import {Task} from '@diary-spo/shared' +import type { Task } from '@diary-spo/shared' import { type ICacheData, retriesForError } from '@helpers' import { objPropertyCopy } from 'src/helpers/objPropertyCopy' import { taskTypeSaveOrGet } from 'src/models/TaskType' import { addNewMarkEvent } from '../../../../worker/notificator/bot' -import {markDelete, markGetByData, markSaveOrGet} from '../../../Mark' +import { markDelete, markGetByData, markSaveOrGet } from '../../../Mark' +import { markValueGetById } from '../../../MarkValue/actions/get/markValueGetById' import { requiredSaveOrGet } from '../../../Required' import type { IScheduleModel } from '../../../Schedule' import type { ITermDetectP } from '../../../Term' import { TaskModel } from '../../model' import { deleteTasks } from '../delete' -import {markValueGetById} from "../../../MarkValue/actions/get/markValueGetById"; export const tasksSaveOrGet = async ( tasks: Task[], @@ -93,14 +93,16 @@ export const tasksSaveOrGet = async ( systemInitiator ) } else { - let deletedMarkRaw = await markGetByData(taskId, authData) + const deletedMarkRaw = await markGetByData(taskId, authData) markDelete(taskId, authData).then(async (count) => { // Игнорируем не системные инициализаторы (т.е. если пользователь уже сам посмотрел) if (count > 0 && systemInitiator && deletedMarkRaw) { - let markValue = await markValueGetById(deletedMarkRaw.markValueId) + const markValue = await markValueGetById( + deletedMarkRaw.markValueId + ) if (markValue) - // Регистрируем событие удаления оценки + // Регистрируем событие удаления оценки addNewMarkEvent({ mark: markValue.value, task, diff --git a/apps/api/src/worker/notificator/bot/botLogic/index.ts b/apps/api/src/worker/notificator/bot/botLogic/index.ts index 41f36cbe..f6a2d3f9 100644 --- a/apps/api/src/worker/notificator/bot/botLogic/index.ts +++ b/apps/api/src/worker/notificator/bot/botLogic/index.ts @@ -1,2 +1,2 @@ export * from './telegramBot' -export * from './registerLogic' \ No newline at end of file +export * from './registerLogic' diff --git a/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts index de381fac..fc4d7c5a 100644 --- a/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts +++ b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts @@ -1,81 +1,80 @@ -import {getCookieFromToken} from "@helpers"; -import {SubscribeModel} from "../../../../models/Subscribe"; -import {DiaryUserModel} from "../../../../models/DiaryUser"; -import TelegramBot from "node-telegram-bot-api"; +import { getCookieFromToken } from '@helpers' +import type TelegramBot from 'node-telegram-bot-api' +import { DiaryUserModel } from '../../../../models/DiaryUser' +import { SubscribeModel } from '../../../../models/Subscribe' export const registerLogic = (bot: TelegramBot | null) => { - if (!bot) - return - bot.on('message', async (msg) => { - const chatId = msg.chat.id + if (!bot) return + bot.on('message', async (msg) => { + const chatId = msg.chat.id - if (!msg.text) bot.sendMessage(chatId, 'Такое сообщение не поддерживается') + if (!msg.text) bot.sendMessage(chatId, 'Такое сообщение не поддерживается') - const command = (msg.text ?? '').split(' ') + const command = (msg.text ?? '').split(' ') - switch (command[0]) { - case '/subscribe': { - if (command.length < 2) { - bot.sendMessage( - chatId, - 'Передайте вторым параметром актуальный токен, чтобы подписаться на уведомления' - ) - return - } - const token = command[1] + switch (command[0]) { + case '/subscribe': { + if (command.length < 2) { + bot.sendMessage( + chatId, + 'Передайте вторым параметром актуальный токен, чтобы подписаться на уведомления' + ) + return + } + const token = command[1] - const auth = await getCookieFromToken(token).catch(() => null) + const auth = await getCookieFromToken(token).catch(() => null) - if (!auth) { - bot.sendMessage(chatId, 'Токен не найден ...') - return - } + if (!auth) { + bot.sendMessage(chatId, 'Токен не найден ...') + return + } - const subscribes = await SubscribeModel.findAll({ - where: { - tgId: chatId - } - }) + const subscribes = await SubscribeModel.findAll({ + where: { + tgId: chatId + } + }) - if (subscribes.length >= 1) { - bot.sendMessage( - chatId, - 'Вы уже подписаны для других аккаунтов. Сначала отпишитесь от остальных (/unsubscribe)' - ) - return - } + if (subscribes.length >= 1) { + bot.sendMessage( + chatId, + 'Вы уже подписаны для других аккаунтов. Сначала отпишитесь от остальных (/unsubscribe)' + ) + return + } - await SubscribeModel.create({ - diaryUserId: auth.localUserId, - tgId: BigInt(chatId) - }) + await SubscribeModel.create({ + diaryUserId: auth.localUserId, + tgId: BigInt(chatId) + }) - const user = await DiaryUserModel.findOne({ - where: { - id: auth.localUserId - } - }) + const user = await DiaryUserModel.findOne({ + where: { + id: auth.localUserId + } + }) - bot.sendMessage( - chatId, - `Вы подписались на аккаунт c ФИО => ${user?.firstName} ${user?.lastName} ${user?.middleName}` - ) - break - } - case '/unsubscribe': - await SubscribeModel.destroy({ - where: { - tgId: chatId - } - }) - bot.sendMessage( - chatId, - 'Вы успешно отписались от всех аккаунтов. Можете привязать новый (/subscribe)' - ) - break - default: - bot.sendMessage(chatId, 'Такой команды нету') - break - } - }) -} \ No newline at end of file + bot.sendMessage( + chatId, + `Вы подписались на аккаунт c ФИО => ${user?.firstName} ${user?.lastName} ${user?.middleName}` + ) + break + } + case '/unsubscribe': + await SubscribeModel.destroy({ + where: { + tgId: chatId + } + }) + bot.sendMessage( + chatId, + 'Вы успешно отписались от всех аккаунтов. Можете привязать новый (/subscribe)' + ) + break + default: + bot.sendMessage(chatId, 'Такой команды нету') + break + } + }) +} diff --git a/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts b/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts index 0a5018f8..9cb54eff 100644 --- a/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts +++ b/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts @@ -1,12 +1,12 @@ import { BOT_TOKEN } from '@config' import TelegramBot from 'node-telegram-bot-api' -import {registerLogic} from "./registerLogic" +import { registerLogic } from './registerLogic' const token = BOT_TOKEN -export const bot = Bun.main.includes('main.ts') || token == 'IGNORE' +export const bot = + Bun.main.includes('main.ts') || token === 'IGNORE' ? null : new TelegramBot(token, { polling: true }) -if (bot) - registerLogic(bot) \ No newline at end of file +if (bot) registerLogic(bot) diff --git a/apps/api/src/worker/notificator/bot/index.ts b/apps/api/src/worker/notificator/bot/index.ts index 43f256b4..2edfccfe 100644 --- a/apps/api/src/worker/notificator/bot/index.ts +++ b/apps/api/src/worker/notificator/bot/index.ts @@ -1,2 +1,2 @@ export * from './botLogic' -export * from './notificationLogic' \ No newline at end of file +export * from './notificationLogic' diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getDetailedInfo.ts b/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getDetailedInfo.ts index e888d6bc..4c89be43 100644 --- a/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getDetailedInfo.ts +++ b/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getDetailedInfo.ts @@ -1,46 +1,44 @@ -import {taskGetFromDB} from "../../../../../models/Task"; -import {MarkEvent} from "../../../types/MarkEvent"; -import {scheduleGetFromDBById} from "../../../../../models/Schedule"; -import {subjectGetFromDBById} from "../../../../../models/Subject"; -import {markValueGetById} from "../../../../../models/MarkValue/actions/get/markValueGetById"; -import {EventDetailedInfo} from "../../../types/EventDetailedInfo"; -import {requiredGetByData} from "../../../../../models/Required"; -import {Grade} from "@diary-spo/shared"; +import { Grade } from '@diary-spo/shared' +import { markValueGetById } from '../../../../../models/MarkValue/actions/get/markValueGetById' +import { requiredGetByData } from '../../../../../models/Required' +import { scheduleGetFromDBById } from '../../../../../models/Schedule' +import { subjectGetFromDBById } from '../../../../../models/Subject' +import { taskGetFromDB } from '../../../../../models/Task' +import type { EventDetailedInfo } from '../../../types/EventDetailedInfo' +import type { MarkEvent } from '../../../types/MarkEvent' -export const getDetailedInfo = async (event: MarkEvent): Promise => { - const task = await taskGetFromDB(event.task.id) +export const getDetailedInfo = async ( + event: MarkEvent +): Promise => { + const task = await taskGetFromDB(event.task.id) - if (!task) - return null + if (!task) return null - const taskRequired = await requiredGetByData(event.diaryUserId, task.id) + const taskRequired = await requiredGetByData(event.diaryUserId, task.id) - if (!taskRequired) - return null + if (!taskRequired) return null - const schedule = await scheduleGetFromDBById(task.scheduleId) + const schedule = await scheduleGetFromDBById(task.scheduleId) - if (!schedule || !schedule?.subjectId) - return null + if (!schedule || !schedule?.subjectId) return null - const subject = await subjectGetFromDBById(schedule.subjectId) + const subject = await subjectGetFromDBById(schedule.subjectId) - if (!subject) - return null + if (!subject) return null - const previousMark = event.previousMarkId - ? (await markValueGetById(event.previousMarkId))?.value ?? null - : null + const previousMark = event.previousMarkId + ? ((await markValueGetById(event.previousMarkId))?.value ?? null) + : null - const isCredit = event.mark === 'Two' && taskRequired.isRequired + const isCredit = event.mark === 'Two' && taskRequired.isRequired - return { - task, - subject, - schedule, - previousMark: previousMark ? Grade[previousMark] : null, - currentMark: isCredit ? 'Д' : Grade[event.mark], - eventDatetime: event.eventDatetime, - status: event.status - } -} \ No newline at end of file + return { + task, + subject, + schedule, + previousMark: previousMark ? Grade[previousMark] : null, + currentMark: isCredit ? 'Д' : Grade[event.mark], + eventDatetime: event.eventDatetime, + status: event.status + } +} diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getSubscribeInfo.ts b/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getSubscribeInfo.ts index 092f44b9..499d0a32 100644 --- a/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getSubscribeInfo.ts +++ b/apps/api/src/worker/notificator/bot/notificationLogic/helpers/getSubscribeInfo.ts @@ -1,18 +1,19 @@ -import {MarkEvent} from "../../../types/MarkEvent"; -import {SubscribeModel} from "../../../../../models/Subscribe"; -import {SubscribeInfo} from "../../../types/SubscribeInfo"; +import { SubscribeModel } from '../../../../../models/Subscribe' +import type { MarkEvent } from '../../../types/MarkEvent' +import type { SubscribeInfo } from '../../../types/SubscribeInfo' -export const getSubscribeInfo = async (event: MarkEvent): Promise => { - const subscribe = await SubscribeModel.findOne({ - where: { - diaryUserId: event.diaryUserId - } - }) +export const getSubscribeInfo = async ( + event: MarkEvent +): Promise => { + const subscribe = await SubscribeModel.findOne({ + where: { + diaryUserId: event.diaryUserId + } + }) - if (!subscribe) - return null + if (!subscribe) return null - return { - tgId: subscribe.tgId - } -} \ No newline at end of file + return { + tgId: subscribe.tgId + } +} diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/index.ts b/apps/api/src/worker/notificator/bot/notificationLogic/index.ts index 33c0a859..c487cfd5 100644 --- a/apps/api/src/worker/notificator/bot/notificationLogic/index.ts +++ b/apps/api/src/worker/notificator/bot/notificationLogic/index.ts @@ -1,2 +1,2 @@ export * from './sendMarkEvent' -export * from './notificationController' \ No newline at end of file +export * from './notificationController' diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/notificationController.ts b/apps/api/src/worker/notificator/bot/notificationLogic/notificationController.ts index 6412a070..df8c3ddc 100644 --- a/apps/api/src/worker/notificator/bot/notificationLogic/notificationController.ts +++ b/apps/api/src/worker/notificator/bot/notificationLogic/notificationController.ts @@ -1,13 +1,13 @@ -import {MarkEvent} from "../../types/MarkEvent"; -import {sendMarkEvent} from "./sendMarkEvent"; +import type { MarkEvent } from '../../types/MarkEvent' +import { sendMarkEvent } from './sendMarkEvent' /** * Регистрирует событие взаимодействия с оценкой * @param event */ export const addNewMarkEvent = async (event: MarkEvent): Promise => { - await sendMarkEvent(event); + await sendMarkEvent(event) } // comment: все события описываются здесь -// напиример, пока что тут только событие на оценки (но в будущем будет и на расписание) \ No newline at end of file +// напиример, пока что тут только событие на оценки (но в будущем будет и на расписание) diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts b/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts index bb8a84fa..678b1add 100644 --- a/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts +++ b/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts @@ -1,24 +1,21 @@ -import {MarkEvent} from "../../types/MarkEvent"; -import {getSubscribeInfo} from "./helpers/getSubscribeInfo"; -import {getDetailedInfo} from "./helpers/getDetailedInfo"; -import {buildMessageByEvent} from "../../messages"; -import {bot} from "../botLogic"; +import { buildMessageByEvent } from '../../messages' +import type { MarkEvent } from '../../types/MarkEvent' +import { bot } from '../botLogic' +import { getDetailedInfo } from './helpers/getDetailedInfo' +import { getSubscribeInfo } from './helpers/getSubscribeInfo' export const sendMarkEvent = async (event: MarkEvent) => { - const subscribe = await getSubscribeInfo(event) + const subscribe = await getSubscribeInfo(event) - if (!subscribe) - return + if (!subscribe) return - const detailedInfo = await getDetailedInfo(event) + const detailedInfo = await getDetailedInfo(event) - if (!detailedInfo) - return + if (!detailedInfo) return - const message = buildMessageByEvent(detailedInfo) + const message = buildMessageByEvent(detailedInfo) - if (!bot || !message) - return + if (!bot || !message) return - bot.sendMessage(String(subscribe.tgId), message) -} \ No newline at end of file + bot.sendMessage(String(subscribe.tgId), message) +} diff --git a/apps/api/src/worker/notificator/config.ts b/apps/api/src/worker/notificator/config.ts index d7a73b23..5effb64c 100644 --- a/apps/api/src/worker/notificator/config.ts +++ b/apps/api/src/worker/notificator/config.ts @@ -1,2 +1,2 @@ // Интервал повторения перепроверки новых оценок -export const INTERVAL_RUN = 2 * 60 // каждые 2 минуты \ No newline at end of file +export const INTERVAL_RUN = 2 * 60 // каждые 2 минуты diff --git a/apps/api/src/worker/notificator/messages/helpers/getDayInfo.ts b/apps/api/src/worker/notificator/messages/helpers/getDayInfo.ts index 8ebe4ed9..a8906566 100644 --- a/apps/api/src/worker/notificator/messages/helpers/getDayInfo.ts +++ b/apps/api/src/worker/notificator/messages/helpers/getDayInfo.ts @@ -1,15 +1,15 @@ -import {getWeekDay} from "./getWeekDay"; -import {EventDetailedInfo} from "../../types/EventDetailedInfo"; +import type { EventDetailedInfo } from '../../types/EventDetailedInfo' +import { getWeekDay } from './getWeekDay' export const getDayInfo = (info: EventDetailedInfo) => { - const date = new Date(info.schedule.date) - const dayName = getWeekDay(date) - const day = `0${date.getUTCDate()}`.slice(-2) - const month = `0${date.getUTCMonth() + 1}`.slice(-2) + const date = new Date(info.schedule.date) + const dayName = getWeekDay(date) + const day = `0${date.getUTCDate()}`.slice(-2) + const month = `0${date.getUTCMonth() + 1}`.slice(-2) - return { - dayName, - day, - month - } -} \ No newline at end of file + return { + dayName, + day, + month + } +} diff --git a/apps/api/src/worker/notificator/messages/helpers/getSmileByMarkValue.ts b/apps/api/src/worker/notificator/messages/helpers/getSmileByMarkValue.ts index d731d92e..4122e20f 100644 --- a/apps/api/src/worker/notificator/messages/helpers/getSmileByMarkValue.ts +++ b/apps/api/src/worker/notificator/messages/helpers/getSmileByMarkValue.ts @@ -1,12 +1,13 @@ -import {AdditionalMarks} from "@diary-spo/shared"; +import type { AdditionalMarks } from '@diary-spo/shared' const emojis: Record = { - 5: '5️⃣', - 4: '4️⃣', - 3: '3️⃣', - 2: '2️⃣', - 'Зч': 'Зч (🥳)', - 'Д': 'Д (🫡)' + 5: '5️⃣', + 4: '4️⃣', + 3: '3️⃣', + 2: '2️⃣', + Зч: 'Зч 🥳', + Д: 'Д 🫡' } -export const getSmileByMarkValue = (mark: AdditionalMarks | number) => emojis[mark] \ No newline at end of file +export const getSmileByMarkValue = (mark: AdditionalMarks | number) => + emojis[mark] diff --git a/apps/api/src/worker/notificator/messages/helpers/getWeekDay.ts b/apps/api/src/worker/notificator/messages/helpers/getWeekDay.ts index ee26d166..d3ef5158 100644 --- a/apps/api/src/worker/notificator/messages/helpers/getWeekDay.ts +++ b/apps/api/src/worker/notificator/messages/helpers/getWeekDay.ts @@ -1,3 +1,3 @@ const days = ['вс', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб'] -export const getWeekDay = (date: Date) => days[date.getDay()] \ No newline at end of file +export const getWeekDay = (date: Date) => days[date.getDay()] diff --git a/apps/api/src/worker/notificator/messages/helpers/index.ts b/apps/api/src/worker/notificator/messages/helpers/index.ts index 3619b108..492bb01c 100644 --- a/apps/api/src/worker/notificator/messages/helpers/index.ts +++ b/apps/api/src/worker/notificator/messages/helpers/index.ts @@ -1,3 +1,3 @@ export * from './getWeekDay' export * from './getDayInfo' -export * from './getSmileByMarkValue' \ No newline at end of file +export * from './getSmileByMarkValue' diff --git a/apps/api/src/worker/notificator/messages/index.ts b/apps/api/src/worker/notificator/messages/index.ts index 1dcfdf18..38f96bce 100644 --- a/apps/api/src/worker/notificator/messages/index.ts +++ b/apps/api/src/worker/notificator/messages/index.ts @@ -1,15 +1,16 @@ -import {EventDetailedInfo} from "../types/EventDetailedInfo"; -import {buildAddMarkMessage} from "./templates/addMark"; -import {buildDeleteMarkMessage} from "./templates/deleteMark"; -import {buildUpdateMarkMessage} from "./templates/updateMark"; +import type { EventDetailedInfo } from '../types/EventDetailedInfo' +import { buildAddMarkMessage } from './templates/addMark' +import { buildDeleteMarkMessage } from './templates/deleteMark' +import { buildUpdateMarkMessage } from './templates/updateMark' const messages = { - 'ADD': buildAddMarkMessage, - 'DELETE': buildDeleteMarkMessage, - 'UPDATE': buildUpdateMarkMessage + ADD: buildAddMarkMessage, + DELETE: buildDeleteMarkMessage, + UPDATE: buildUpdateMarkMessage } -export const buildMessageByEvent = (info: EventDetailedInfo) => messages[info.status](info) +export const buildMessageByEvent = (info: EventDetailedInfo) => + messages[info.status](info) export * from './helpers' diff --git a/apps/api/src/worker/notificator/messages/templates/addMark/index.ts b/apps/api/src/worker/notificator/messages/templates/addMark/index.ts index c5e5e9f8..70c952a4 100644 --- a/apps/api/src/worker/notificator/messages/templates/addMark/index.ts +++ b/apps/api/src/worker/notificator/messages/templates/addMark/index.ts @@ -1,15 +1,15 @@ -import {EventDetailedInfo} from "../../../types/EventDetailedInfo"; -import {getDayInfo} from "../../helpers"; -import {getSmileByMarkValue} from "../../helpers"; +import type { EventDetailedInfo } from '../../../types/EventDetailedInfo' +import { getDayInfo } from '../../helpers' +import { getSmileByMarkValue } from '../../helpers' export const buildAddMarkMessage = (info: EventDetailedInfo) => { - const markValue = info.currentMark + const markValue = info.currentMark - const dayInfo = getDayInfo(info) + const dayInfo = getDayInfo(info) - const subjectName = info.subject.name + const subjectName = info.subject.name - const emoji = getSmileByMarkValue(markValue) + const emoji = getSmileByMarkValue(markValue) - return `${emoji} на ${dayInfo.day}.${dayInfo.month} (${dayInfo.dayName}) по ${subjectName}` -} \ No newline at end of file + return `${emoji} на ${dayInfo.day}.${dayInfo.month} (${dayInfo.dayName}) по ${subjectName}` +} diff --git a/apps/api/src/worker/notificator/messages/templates/deleteMark/index.ts b/apps/api/src/worker/notificator/messages/templates/deleteMark/index.ts index 5954a1e2..cdaa8d6c 100644 --- a/apps/api/src/worker/notificator/messages/templates/deleteMark/index.ts +++ b/apps/api/src/worker/notificator/messages/templates/deleteMark/index.ts @@ -1,15 +1,15 @@ -import {EventDetailedInfo} from "../../../types/EventDetailedInfo"; -import {getDayInfo} from "../../helpers"; -import {getSmileByMarkValue} from "../../helpers"; +import type { EventDetailedInfo } from '../../../types/EventDetailedInfo' +import { getDayInfo } from '../../helpers' +import { getSmileByMarkValue } from '../../helpers' export const buildDeleteMarkMessage = (info: EventDetailedInfo) => { - const markValue = info.currentMark + const markValue = info.currentMark - const dayInfo = getDayInfo(info) + const dayInfo = getDayInfo(info) - const subjectName = info.subject.name + const subjectName = info.subject.name - const emoji = getSmileByMarkValue(markValue) + const emoji = getSmileByMarkValue(markValue) - return `Убрана ${emoji} на ${dayInfo.day}.${dayInfo.month} (${dayInfo.dayName}) по ${subjectName}` -} \ No newline at end of file + return `Убрана ${emoji} на ${dayInfo.day}.${dayInfo.month} (${dayInfo.dayName}) по ${subjectName}` +} diff --git a/apps/api/src/worker/notificator/messages/templates/updateMark/index.ts b/apps/api/src/worker/notificator/messages/templates/updateMark/index.ts index e3ccbbf2..d65a219b 100644 --- a/apps/api/src/worker/notificator/messages/templates/updateMark/index.ts +++ b/apps/api/src/worker/notificator/messages/templates/updateMark/index.ts @@ -1,20 +1,19 @@ -import {EventDetailedInfo} from "../../../types/EventDetailedInfo"; -import {getDayInfo} from "../../helpers"; -import {getSmileByMarkValue} from "../../helpers"; +import type { EventDetailedInfo } from '../../../types/EventDetailedInfo' +import { getDayInfo } from '../../helpers' +import { getSmileByMarkValue } from '../../helpers' export const buildUpdateMarkMessage = (info: EventDetailedInfo) => { - const markValue = info.currentMark - const previousMarkValue = info.previousMark + const markValue = info.currentMark + const previousMarkValue = info.previousMark - if (!previousMarkValue) - return null + if (!previousMarkValue) return null - const dayInfo = getDayInfo(info) + const dayInfo = getDayInfo(info) - const subjectName = info.subject.name + const subjectName = info.subject.name - const emoji = getSmileByMarkValue(markValue) - const previousEmoji = getSmileByMarkValue(previousMarkValue) + const emoji = getSmileByMarkValue(markValue) + const previousEmoji = getSmileByMarkValue(previousMarkValue) - return `${emoji} (было ${previousEmoji}) на ${dayInfo.day}.${dayInfo.month} (${dayInfo.dayName}) по ${subjectName}` -} \ No newline at end of file + return `${emoji} (было ${previousEmoji}) на ${dayInfo.day}.${dayInfo.month} (${dayInfo.dayName}) по ${subjectName}` +} diff --git a/apps/api/src/worker/notificator/types/EventDetailedInfo.ts b/apps/api/src/worker/notificator/types/EventDetailedInfo.ts index 042dda99..89ecf522 100644 --- a/apps/api/src/worker/notificator/types/EventDetailedInfo.ts +++ b/apps/api/src/worker/notificator/types/EventDetailedInfo.ts @@ -1,17 +1,17 @@ -import {IScheduleModel} from "../../../models/Schedule"; -import {ITaskModel} from "../../../models/Task"; -import {ISubjectModelType} from "../../../models/Subject"; -import {type AdditionalMarks} from "@diary-spo/shared"; +import type { AdditionalMarks } from '@diary-spo/shared' +import type { IScheduleModel } from '../../../models/Schedule' +import type { ISubjectModelType } from '../../../models/Subject' +import type { ITaskModel } from '../../../models/Task' export interface EventDetailedInfo { - // Информация об источнике события - subject: ISubjectModelType - schedule: IScheduleModel - task: ITaskModel - // Информация об изменениях - previousMark: AdditionalMarks | number | null - currentMark: AdditionalMarks | number - status: "ADD" | "DELETE" | "UPDATE" - // Доп. инфа - eventDatetime: Date -} \ No newline at end of file + // Информация об источнике события + subject: ISubjectModelType + schedule: IScheduleModel + task: ITaskModel + // Информация об изменениях + previousMark: AdditionalMarks | number | null + currentMark: AdditionalMarks | number + status: 'ADD' | 'DELETE' | 'UPDATE' + // Доп. инфа + eventDatetime: Date +} diff --git a/apps/api/src/worker/notificator/types/MarkEvent.ts b/apps/api/src/worker/notificator/types/MarkEvent.ts index 43f13df4..c6dad1a1 100644 --- a/apps/api/src/worker/notificator/types/MarkEvent.ts +++ b/apps/api/src/worker/notificator/types/MarkEvent.ts @@ -1,10 +1,10 @@ -import {MarkKeys, Task} from "@diary-spo/shared"; +import type { MarkKeys, Task } from '@diary-spo/shared' export interface MarkEvent { - diaryUserId: bigint - task: Task - mark: MarkKeys - previousMarkId: number | null - status: 'ADD' | 'DELETE' | 'UPDATE' - eventDatetime: Date -} \ No newline at end of file + diaryUserId: bigint + task: Task + mark: MarkKeys + previousMarkId: number | null + status: 'ADD' | 'DELETE' | 'UPDATE' + eventDatetime: Date +} diff --git a/apps/api/src/worker/notificator/types/SubscribeInfo.ts b/apps/api/src/worker/notificator/types/SubscribeInfo.ts index 93f80fb9..b1ab37b2 100644 --- a/apps/api/src/worker/notificator/types/SubscribeInfo.ts +++ b/apps/api/src/worker/notificator/types/SubscribeInfo.ts @@ -1,3 +1,3 @@ export interface SubscribeInfo { - tgId: bigint -} \ No newline at end of file + tgId: bigint +} From e8539fb0873f2e53421c643e2e5a17440a89684d Mon Sep 17 00:00:00 2001 From: zamelane Date: Wed, 27 Nov 2024 19:03:19 +0700 Subject: [PATCH 04/12] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B2=20=D0=BE=D1=84=D0=BE=D1=80=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/models/Subscribe/model.ts | 7 +++++++ .../notificator/bot/botLogic/registerLogic.ts | 17 ++++++++++++++--- apps/api/src/worker/notificator/index.ts | 13 +++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/apps/api/src/models/Subscribe/model.ts b/apps/api/src/models/Subscribe/model.ts index 4ccbdebf..a5c2c358 100644 --- a/apps/api/src/models/Subscribe/model.ts +++ b/apps/api/src/models/Subscribe/model.ts @@ -8,6 +8,7 @@ import type { IModelPrototypeNoId } from '../types' export type SubscribeModelType = { diaryUserId: bigint tgId: bigint + preActionsIsSuccess: boolean } export type ISubscribeModelType = IModelPrototypeNoId @@ -26,6 +27,12 @@ const subscribeModel = sequelize.define('subscribe', { primaryKey: true, allowNull: false, unique: true + }, + preActionsIsSuccess: { + type: DataTypes.BOOLEAN, + primaryKey: false, + allowNull: false, + unique: true } }) diff --git a/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts index fc4d7c5a..d6bc0c1c 100644 --- a/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts +++ b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts @@ -2,6 +2,7 @@ import { getCookieFromToken } from '@helpers' import type TelegramBot from 'node-telegram-bot-api' import { DiaryUserModel } from '../../../../models/DiaryUser' import { SubscribeModel } from '../../../../models/Subscribe' +import {INTERVAL_RUN} from "../../config"; export const registerLogic = (bot: TelegramBot | null) => { if (!bot) return @@ -46,7 +47,8 @@ export const registerLogic = (bot: TelegramBot | null) => { await SubscribeModel.create({ diaryUserId: auth.localUserId, - tgId: BigInt(chatId) + tgId: BigInt(chatId), + preActionsIsSuccess: false }) const user = await DiaryUserModel.findOne({ @@ -57,7 +59,11 @@ export const registerLogic = (bot: TelegramBot | null) => { bot.sendMessage( chatId, - `Вы подписались на аккаунт c ФИО => ${user?.firstName} ${user?.lastName} ${user?.middleName}` + `${user?.firstName} ${user?.lastName}! Вы успешно подписались на уведомления.` + + `\nПрежде чем Вы начнёте получать уведомления, нам нужно извлечь все ваши оценки (это просиходит примерно каждые ${INTERVAL_RUN} секунд).` + + `\nПо окончанию подготовительных процедур, мы уведомим Вас о готовности принимать уведомления.` + + `\nСпасибо, что выбираете нас!`, + {parse_mode: 'HTML'} ) break } @@ -73,7 +79,12 @@ export const registerLogic = (bot: TelegramBot | null) => { ) break default: - bot.sendMessage(chatId, 'Такой команды нету') + bot.sendMessage(chatId, + `Этой команды нету, но есть такие:` + + `\n/subscribe [token] — подписаться на уведомления по токену` + + `\n/unsubscribe — отписаться от уведомлений`, + {parse_mode:"HTML"} + ) break } }) diff --git a/apps/api/src/worker/notificator/index.ts b/apps/api/src/worker/notificator/index.ts index 566d38ac..b6761c54 100644 --- a/apps/api/src/worker/notificator/index.ts +++ b/apps/api/src/worker/notificator/index.ts @@ -5,6 +5,7 @@ import { getCurrPerformance } from '../../routes/performance.current/service' import { checkInterval } from '../utils/checkInterval' import { logger } from '../utils/logger' import { INTERVAL_RUN } from './config' +import {bot} from "./bot"; let lastRunning: Date | null = null const log = logger('notificator') @@ -32,6 +33,18 @@ export const notificatorChecker = async (): Promise => { const cacheData = await getCookieFromToken(auth.token) + if (!subscribe.preActionsIsSuccess) { + getCurrPerformance(cacheData, false).then(() => { + if (!bot) + return + subscribe.preActionsIsSuccess = true + subscribe.save() + bot.sendMessage(String(subscribe.tgId), `Мы загрузили ваши оценки. Теперь вы будете получать уведомления!`) + }) + continue + } + await getCurrPerformance(cacheData, true) } + log(`Проход завершён. Следующий через ${INTERVAL_RUN} секунд`) } From ee9ad0ddd2e98f7163c6084cb5ec1371e11e7232 Mon Sep 17 00:00:00 2001 From: zamelane Date: Wed, 27 Nov 2024 22:21:32 +0700 Subject: [PATCH 05/12] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B3=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BA=20=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=BE=D1=86=D0=B5=D0=BD=D0=BD=D0=BE=D0=B9=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=BA=D0=B5=20=D0=BD=D0=B0=20=D1=83=D0=B2?= =?UTF-8?q?=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/.env.example | 2 +- apps/api/src/helpers/generateToken.ts | 12 ++- apps/api/src/models/Auth/model.ts | 10 ++- apps/api/src/models/DiaryUser/methods.ts | 6 +- apps/api/src/types/index.ts | 4 + .../notificator/bot/botLogic/registerLogic.ts | 80 ++++++++++++++----- apps/shared/src/api/self/index.ts | 1 + apps/web/.env.development | 1 + apps/web/.env.example | 1 + apps/web/src/pages/LoginForm/helpers/index.ts | 4 +- apps/web/src/pages/Settings/Actions/index.tsx | 16 +++- apps/web/src/shared/api/client.ts | 20 ++++- apps/web/src/shared/config/env.ts | 1 + 13 files changed, 129 insertions(+), 29 deletions(-) diff --git a/apps/api/.env.example b/apps/api/.env.example index 4800b49c..89deea7d 100755 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -12,7 +12,7 @@ DATABASE_PORT=5432 DATABASE_NAME='databaseName' DATABASE_USERNAME='user' DATABASE_PASSWORD='password' -BOT_TOKEN='' +BOT_TOKEN='IGNORE' POSTGRES_USER=postgres POSTGRES_PW=postgres diff --git a/apps/api/src/helpers/generateToken.ts b/apps/api/src/helpers/generateToken.ts index 4f447b2c..08fa69c1 100755 --- a/apps/api/src/helpers/generateToken.ts +++ b/apps/api/src/helpers/generateToken.ts @@ -2,6 +2,7 @@ import { API_CODES, ApiError } from '@api' import { formatDate } from '@utils' import { suid } from 'rand-token' import { AuthModel } from '../models/Auth' +import type { TokenInfo } from '../types' /** * Генерирует токен и вставляет в базу @@ -9,14 +10,16 @@ import { AuthModel } from '../models/Auth' * @param diaryUserId * @returns {string} token */ -export const generateToken = async (diaryUserId: bigint): Promise => { +export const generateToken = async ( + diaryUserId: bigint +): Promise => { // Генерируем токен const token = suid(16) const formattedDate = formatDate(new Date().toISOString()) // TODO: сделать метод рядом с моделью для создания и использовать тут - await AuthModel.create({ + const auth = await AuthModel.create({ diaryUserId, token, lastUsedDate: formattedDate @@ -24,5 +27,8 @@ export const generateToken = async (diaryUserId: bigint): Promise => { throw new ApiError('Error insert token!', API_CODES.INTERNAL_SERVER_ERROR) }) - return token + return { + token, + tokenId: auth.id + } } diff --git a/apps/api/src/models/Auth/model.ts b/apps/api/src/models/Auth/model.ts index 841b4381..b3eba4ef 100755 --- a/apps/api/src/models/Auth/model.ts +++ b/apps/api/src/models/Auth/model.ts @@ -3,17 +3,23 @@ import { DataTypes } from 'sequelize' import { sequelize } from '@db' import { DiaryUserModel } from '../DiaryUser' -import type { IModelPrototypeNoId } from '../types' +import type { IModelPrototype, IModelPrototypeNoId } from '../types' export type AuthModelType = { + id: bigint diaryUserId: bigint token: string lastUsedDate: string } -export type IAuthModel = IModelPrototypeNoId +export type IAuthModel = IModelPrototype export const AuthModel = sequelize.define('auth', { + id: { + type: DataTypes.BIGINT, + autoIncrement: true, + primaryKey: true + }, diaryUserId: { type: DataTypes.BIGINT, allowNull: false, diff --git a/apps/api/src/models/DiaryUser/methods.ts b/apps/api/src/models/DiaryUser/methods.ts index 17af7379..aaa53878 100644 --- a/apps/api/src/models/DiaryUser/methods.ts +++ b/apps/api/src/models/DiaryUser/methods.ts @@ -1,4 +1,5 @@ import type { ResponseLogin } from '@diary-spo/shared' +import type { TokenInfo } from '../../types' import type { IGroupModel } from '../Group' import type { ISPOModel } from '../SPO' import type { IDiaryUserModel } from './model' @@ -7,7 +8,7 @@ export const getFormattedDiaryUserData = ( diaryUser: IDiaryUserModel, spo: ISPOModel, group: IGroupModel, - token: string + token: TokenInfo ): ResponseLogin => ({ id: diaryUser.id, groupId: diaryUser.groupId, @@ -23,5 +24,6 @@ export const getFormattedDiaryUserData = ( lastName: diaryUser.lastName, middleName: diaryUser.middleName, spoId: spo.id, - token + token: token.token, + tokenId: token.tokenId }) diff --git a/apps/api/src/types/index.ts b/apps/api/src/types/index.ts index 415d9c48..e0539714 100755 --- a/apps/api/src/types/index.ts +++ b/apps/api/src/types/index.ts @@ -13,3 +13,7 @@ export interface Token { } export type WithToken = With + +export interface TokenInfo extends Token { + tokenId: bigint +} diff --git a/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts index d6bc0c1c..88625333 100644 --- a/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts +++ b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts @@ -1,8 +1,10 @@ -import { getCookieFromToken } from '@helpers' +// @ts-ignore +import { b64 } from '@diary-spo/crypto' import type TelegramBot from 'node-telegram-bot-api' +import { AuthModel } from '../../../../models/Auth' import { DiaryUserModel } from '../../../../models/DiaryUser' import { SubscribeModel } from '../../../../models/Subscribe' -import {INTERVAL_RUN} from "../../config"; +import { INTERVAL_RUN } from '../../config' export const registerLogic = (bot: TelegramBot | null) => { if (!bot) return @@ -22,12 +24,56 @@ export const registerLogic = (bot: TelegramBot | null) => { ) return } - const token = command[1] - const auth = await getCookieFromToken(token).catch(() => null) + // TODO: потом по-нормальному вынести + let tokenSecure = '' + try { + tokenSecure = atob(command[1]) + } catch { + bot.sendMessage( + chatId, + 'Вы что-то не то шлёте и всё ломаете. В бан захотели?' + ) + return + } + const secureTokenParams = tokenSecure.split(':') + + if (secureTokenParams.length !== 2 && !Number(secureTokenParams[0])) { + bot.sendMessage( + chatId, + 'У вашего токена неверная структура. В бан захотел(-а)?' + ) + return + } + + const auth = await AuthModel.findOne({ + where: { + id: secureTokenParams[0] + } + }) if (!auth) { - bot.sendMessage(chatId, 'Токен не найден ...') + bot.sendMessage(chatId, 'Переданная авторизация не найдена ...') + return + } + + // Проверяем переданную пользователем авторизацию + const tokenObject = { + token: auth.token, + date: new Date().toISOString().substring(0, 10) + } + const secureToken = await b64(JSON.stringify(tokenObject)) + + if (secureToken !== secureTokenParams[1]) { + bot.sendMessage( + chatId, + `Ваш токен какой-то не такой. Если вы ничего не трогали, то проблема у нас.\nПожалуйста, покажите это сообщение разработчикам.\nDebug info: ${btoa( + JSON.stringify({ + tokenSecure, + currServerDate: new Date().toISOString() + }) + )}` + ) return } @@ -40,30 +86,27 @@ export const registerLogic = (bot: TelegramBot | null) => { if (subscribes.length >= 1) { bot.sendMessage( chatId, - 'Вы уже подписаны для других аккаунтов. Сначала отпишитесь от остальных (/unsubscribe)' + 'Вы уже подписаны на уведомления. Сначала отпишитесь (/unsubscribe)' ) return } await SubscribeModel.create({ - diaryUserId: auth.localUserId, + diaryUserId: auth.diaryUserId, tgId: BigInt(chatId), preActionsIsSuccess: false }) const user = await DiaryUserModel.findOne({ where: { - id: auth.localUserId + id: auth.diaryUserId } }) bot.sendMessage( chatId, - `${user?.firstName} ${user?.lastName}! Вы успешно подписались на уведомления.` - + `\nПрежде чем Вы начнёте получать уведомления, нам нужно извлечь все ваши оценки (это просиходит примерно каждые ${INTERVAL_RUN} секунд).` - + `\nПо окончанию подготовительных процедур, мы уведомим Вас о готовности принимать уведомления.` - + `\nСпасибо, что выбираете нас!`, - {parse_mode: 'HTML'} + `${user?.firstName} ${user?.lastName}! Вы успешно подписались на уведомления.\nПрежде чем Вы начнёте получать уведомления, нам нужно извлечь все ваши оценки (это просиходит примерно каждые ${INTERVAL_RUN} секунд).\nПо окончанию подготовительных процедур, мы уведомим Вас о готовности принимать уведомления.\nСпасибо, что выбираете нас!`, + { parse_mode: 'HTML' } ) break } @@ -79,11 +122,12 @@ export const registerLogic = (bot: TelegramBot | null) => { ) break default: - bot.sendMessage(chatId, - `Этой команды нету, но есть такие:` - + `\n/subscribe [token] — подписаться на уведомления по токену` - + `\n/unsubscribe — отписаться от уведомлений`, - {parse_mode:"HTML"} + bot.sendMessage( + chatId, + 'Этой команды нету, но есть такие:' + + '\n/subscribe [token] — подписаться на уведомления по токену' + + '\n/unsubscribe — отписаться от уведомлений', + { parse_mode: 'HTML' } ) break } diff --git a/apps/shared/src/api/self/index.ts b/apps/shared/src/api/self/index.ts index 8b41954f..8d0adb74 100755 --- a/apps/shared/src/api/self/index.ts +++ b/apps/shared/src/api/self/index.ts @@ -28,4 +28,5 @@ export type ResponseLogin = Person & { birthday: string groupName: string token: string + tokenId: bigint } diff --git a/apps/web/.env.development b/apps/web/.env.development index ad9a2bc7..144984b3 100755 --- a/apps/web/.env.development +++ b/apps/web/.env.development @@ -1,6 +1,7 @@ VITE_SERVER_URLS=http://localhost:3003,http://127.0.0.1:3003 VITE_SERVER_URL=http://localhost:3003 # http://localhost:3003,http://127.0.0.1:3003, +VITE_TG_BOT_URL=https://t.me/diary_notifications_bot VITE_MODE=dev VITE_NODE_ENV=development NODE_ENV=development diff --git a/apps/web/.env.example b/apps/web/.env.example index e54b41a9..b24e4a9b 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,5 +1,6 @@ VITE_SERVER_URLS=http://localhost:3003,http://127.0.0.1:3003 VITE_SERVER_URL=http://localhost:3003 +VITE_TG_BOT_URL=https://t.me/contact VITE_MODE=dev VITE_NODE_ENV=development diff --git a/apps/web/src/pages/LoginForm/helpers/index.ts b/apps/web/src/pages/LoginForm/helpers/index.ts index 68492929..168999b3 100755 --- a/apps/web/src/pages/LoginForm/helpers/index.ts +++ b/apps/web/src/pages/LoginForm/helpers/index.ts @@ -4,6 +4,7 @@ import { setToken } from '../../../shared/api/client.ts' export const saveData = (basePath: ResponseLogin) => { const userId = String(basePath.id) const token = basePath.token + const tokenId = basePath.tokenId const name = `${String(basePath.lastName)} ${String( basePath.firstName )} ${String(basePath.middleName)}` @@ -18,7 +19,8 @@ export const saveData = (basePath: ResponseLogin) => { name, org, city, - group + group, + tokenId } setToken(token) diff --git a/apps/web/src/pages/Settings/Actions/index.tsx b/apps/web/src/pages/Settings/Actions/index.tsx index 62ce8023..ba7cdd23 100755 --- a/apps/web/src/pages/Settings/Actions/index.tsx +++ b/apps/web/src/pages/Settings/Actions/index.tsx @@ -1,7 +1,8 @@ import { Icon28DoorArrowRightOutline, Icon28HomeArrowDownOutline, - Icon28IncognitoOutline + Icon28IncognitoOutline, + Icon28Notifications } from '@vkontakte/icons' import bridge from '@vkontakte/vk-bridge' import { useRouteNavigator } from '@vkontakte/vk-mini-apps-router' @@ -11,6 +12,8 @@ import { useEffect, useRef, useState } from 'react' import { logOut } from '../../../shared' import { useSnackbar } from '../../../shared/hooks' +import { getSecureToken } from '../../../shared/api/client.ts' +import { TG_BOT_URL } from '../../../shared/config' import TechInfo from './TechInfo.tsx' const Actions = () => { @@ -106,6 +109,17 @@ const Actions = () => { > Показывать тех. информацию + } + onClick={async () => + window.open( + `${TG_BOT_URL}?text=/subscribe ${await getSecureToken()}`, + '_blank' + ) + } + > + Подключить уведомления + } onClick={() => routeNavigator.showPopout(logOutPopup)} diff --git a/apps/web/src/shared/api/client.ts b/apps/web/src/shared/api/client.ts index 179cfec0..e8eb51c2 100644 --- a/apps/web/src/shared/api/client.ts +++ b/apps/web/src/shared/api/client.ts @@ -2,6 +2,7 @@ import { treaty } from '@elysiajs/eden' import type { App } from '@diary-spo/api/src/main.ts' +import { b64 } from '@diary-spo/crypto' import { API_URL } from '../config' /** @@ -13,7 +14,24 @@ export const setToken = (token: string) => { } export const getToken = (): string | null => { - return globalToken + return globalToken ?? localStorage.getItem('token') +} + +export const getSecureToken = async () => { + const data = localStorage.getItem('data') + + if (!data) return + + const tokenId = (await JSON.parse(data)).tokenId + const token = getToken() + + const tokenObject = { + token, + date: new Date().toISOString().substring(0, 10) + } + const secureToken = await b64(JSON.stringify(tokenObject)) + + return btoa(`${tokenId}:${secureToken}`) } // @TODO: move to config diff --git a/apps/web/src/shared/config/env.ts b/apps/web/src/shared/config/env.ts index 35f9ccd3..0f3b6de7 100755 --- a/apps/web/src/shared/config/env.ts +++ b/apps/web/src/shared/config/env.ts @@ -4,6 +4,7 @@ export const MODE = import.meta.env.VITE_MODE export const ADMIN_PAGE = import.meta.env.VITE_ADMIN_PAGE_URL export const BETA_VERSION = import.meta.env.VITE_BETA_VERSION export const API_URL = import.meta.env.VITE_SERVER_URL +export const TG_BOT_URL = import.meta.env.VITE_TG_BOT_URL export const Mode = { DEV: 'dev', From 1c6ccf6adf110226ceb551ac3584f74d2cd6e8eb Mon Sep 17 00:00:00 2001 From: zamelane Date: Wed, 27 Nov 2024 22:23:04 +0700 Subject: [PATCH 06/12] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B3=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BA=20=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=BE=D1=86=D0=B5=D0=BD=D0=BD=D0=BE=D0=B9=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=BA=D0=B5=20=D0=BD=D0=B0=20=D1=83=D0=B2?= =?UTF-8?q?=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/worker/notificator/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/api/src/worker/notificator/index.ts b/apps/api/src/worker/notificator/index.ts index b6761c54..174ce091 100644 --- a/apps/api/src/worker/notificator/index.ts +++ b/apps/api/src/worker/notificator/index.ts @@ -4,8 +4,8 @@ import { SubscribeModel } from '../../models/Subscribe' import { getCurrPerformance } from '../../routes/performance.current/service' import { checkInterval } from '../utils/checkInterval' import { logger } from '../utils/logger' +import { bot } from './bot' import { INTERVAL_RUN } from './config' -import {bot} from "./bot"; let lastRunning: Date | null = null const log = logger('notificator') @@ -35,11 +35,13 @@ export const notificatorChecker = async (): Promise => { if (!subscribe.preActionsIsSuccess) { getCurrPerformance(cacheData, false).then(() => { - if (!bot) - return + if (!bot) return subscribe.preActionsIsSuccess = true subscribe.save() - bot.sendMessage(String(subscribe.tgId), `Мы загрузили ваши оценки. Теперь вы будете получать уведомления!`) + bot.sendMessage( + String(subscribe.tgId), + 'Мы загрузили ваши оценки. Теперь вы будете получать уведомления!' + ) }) continue } From ccc9b5ad0723191140970a1f90327fa2a7369967 Mon Sep 17 00:00:00 2001 From: zamelane Date: Wed, 27 Nov 2024 23:10:17 +0700 Subject: [PATCH 07/12] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D1=82=D1=8C=20=D0=BD=D0=B0=20puregram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/package.json | 2 +- .../notificator/bot/botLogic/registerLogic.ts | 37 ++++++++---------- .../notificator/bot/botLogic/telegramBot.ts | 9 +++-- apps/api/src/worker/notificator/index.ts | 8 ++-- bun.lockb | Bin 206720 -> 153064 bytes 5 files changed, 27 insertions(+), 29 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 6981ee0b..1a9f00d1 100755 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -24,9 +24,9 @@ "elysia-helmet": "^2.0.0", "ky": "^1.7.2", "node-rsa": "^1.1.1", - "node-telegram-bot-api": "^0.66.0", "pg": "^8.13.1", "pg-hstore": "^2.3.4", + "puregram": "^2.26.8", "rand-token": "^1.0.1", "sequelize": "^6.37.5", "sequelize-simple-cache": "^1.3.5" diff --git a/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts index 88625333..4b1755de 100644 --- a/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts +++ b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts @@ -1,25 +1,27 @@ // @ts-ignore import { b64 } from '@diary-spo/crypto' -import type TelegramBot from 'node-telegram-bot-api' import { AuthModel } from '../../../../models/Auth' import { DiaryUserModel } from '../../../../models/DiaryUser' import { SubscribeModel } from '../../../../models/Subscribe' import { INTERVAL_RUN } from '../../config' +import {Telegram} from "puregram"; -export const registerLogic = (bot: TelegramBot | null) => { +export const registerLogic = (bot: Telegram | null) => { if (!bot) return - bot.on('message', async (msg) => { + bot.updates.on('message', async (msg) => { const chatId = msg.chat.id - if (!msg.text) bot.sendMessage(chatId, 'Такое сообщение не поддерживается') + if (!msg.text) { + msg.reply('Такое сообщение не поддерживается') + return + } const command = (msg.text ?? '').split(' ') switch (command[0]) { case '/subscribe': { if (command.length < 2) { - bot.sendMessage( - chatId, + msg.reply( 'Передайте вторым параметром актуальный токен, чтобы подписаться на уведомления' ) return @@ -30,8 +32,7 @@ export const registerLogic = (bot: TelegramBot | null) => { try { tokenSecure = atob(command[1]) } catch { - bot.sendMessage( - chatId, + msg.reply( 'Вы что-то не то шлёте и всё ломаете. В бан захотели?' ) return @@ -39,8 +40,7 @@ export const registerLogic = (bot: TelegramBot | null) => { const secureTokenParams = tokenSecure.split(':') if (secureTokenParams.length !== 2 && !Number(secureTokenParams[0])) { - bot.sendMessage( - chatId, + msg.reply( 'У вашего токена неверная структура. В бан захотел(-а)?' ) return @@ -53,7 +53,7 @@ export const registerLogic = (bot: TelegramBot | null) => { }) if (!auth) { - bot.sendMessage(chatId, 'Переданная авторизация не найдена ...') + msg.reply('Переданная авторизация не найдена ...') return } @@ -65,8 +65,7 @@ export const registerLogic = (bot: TelegramBot | null) => { const secureToken = await b64(JSON.stringify(tokenObject)) if (secureToken !== secureTokenParams[1]) { - bot.sendMessage( - chatId, + msg.reply( `Ваш токен какой-то не такой. Если вы ничего не трогали, то проблема у нас.\nПожалуйста, покажите это сообщение разработчикам.\nDebug info: ${btoa( JSON.stringify({ tokenSecure, @@ -84,8 +83,7 @@ export const registerLogic = (bot: TelegramBot | null) => { }) if (subscribes.length >= 1) { - bot.sendMessage( - chatId, + msg.reply( 'Вы уже подписаны на уведомления. Сначала отпишитесь (/unsubscribe)' ) return @@ -103,8 +101,7 @@ export const registerLogic = (bot: TelegramBot | null) => { } }) - bot.sendMessage( - chatId, + msg.reply( `${user?.firstName} ${user?.lastName}! Вы успешно подписались на уведомления.\nПрежде чем Вы начнёте получать уведомления, нам нужно извлечь все ваши оценки (это просиходит примерно каждые ${INTERVAL_RUN} секунд).\nПо окончанию подготовительных процедур, мы уведомим Вас о готовности принимать уведомления.\nСпасибо, что выбираете нас!`, { parse_mode: 'HTML' } ) @@ -116,14 +113,12 @@ export const registerLogic = (bot: TelegramBot | null) => { tgId: chatId } }) - bot.sendMessage( - chatId, + msg.reply( 'Вы успешно отписались от всех аккаунтов. Можете привязать новый (/subscribe)' ) break default: - bot.sendMessage( - chatId, + msg.reply( 'Этой команды нету, но есть такие:' + '\n/subscribe [token] — подписаться на уведомления по токену' + '\n/unsubscribe — отписаться от уведомлений', diff --git a/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts b/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts index 9cb54eff..24b46f04 100644 --- a/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts +++ b/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts @@ -1,12 +1,15 @@ import { BOT_TOKEN } from '@config' -import TelegramBot from 'node-telegram-bot-api' import { registerLogic } from './registerLogic' +import {Telegram} from "puregram"; const token = BOT_TOKEN export const bot = Bun.main.includes('main.ts') || token === 'IGNORE' ? null - : new TelegramBot(token, { polling: true }) + : Telegram.fromToken(token) -if (bot) registerLogic(bot) +if (bot) { + registerLogic(bot) + bot.updates.startPolling() +} diff --git a/apps/api/src/worker/notificator/index.ts b/apps/api/src/worker/notificator/index.ts index 174ce091..192543d9 100644 --- a/apps/api/src/worker/notificator/index.ts +++ b/apps/api/src/worker/notificator/index.ts @@ -38,10 +38,10 @@ export const notificatorChecker = async (): Promise => { if (!bot) return subscribe.preActionsIsSuccess = true subscribe.save() - bot.sendMessage( - String(subscribe.tgId), - 'Мы загрузили ваши оценки. Теперь вы будете получать уведомления!' - ) + bot.api.sendMessage({ + chat_id: String(subscribe.tgId), + text: 'Мы загрузили ваши оценки. Теперь вы будете получать уведомления!' + }) }) continue } diff --git a/bun.lockb b/bun.lockb index b570d85981b8b2706f25ff4b2c5cc892e07d4b18..1954b03a3780ae2e1e1f3e308897aa00b9611713 100755 GIT binary patch delta 8027 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 Date: Wed, 27 Nov 2024 23:18:36 +0700 Subject: [PATCH 08/12] =?UTF-8?q?=D0=9C=D0=B0=D0=BB=D0=B5=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=B8=D0=B9=20=D0=B1=D0=B0=D0=B3=20)))?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/models/Subscribe/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/models/Subscribe/model.ts b/apps/api/src/models/Subscribe/model.ts index a5c2c358..c3bdf6c2 100644 --- a/apps/api/src/models/Subscribe/model.ts +++ b/apps/api/src/models/Subscribe/model.ts @@ -32,7 +32,7 @@ const subscribeModel = sequelize.define('subscribe', { type: DataTypes.BOOLEAN, primaryKey: false, allowNull: false, - unique: true + unique: false } }) From 74d51558af96d75c166a33106c1a246853ec8a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=BD=D0=B8=D0=BD?= <39529518+Zamelane@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:39:32 +0700 Subject: [PATCH 09/12] Update sendMarkEvent.ts --- .../notificator/bot/notificationLogic/sendMarkEvent.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts b/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts index 678b1add..4c11c0b9 100644 --- a/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts +++ b/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts @@ -17,5 +17,8 @@ export const sendMarkEvent = async (event: MarkEvent) => { if (!bot || !message) return - bot.sendMessage(String(subscribe.tgId), message) + bot.api.sendMessage({ + chat_id: String(subscribe.tgId), + text: message + }) } From 80e4cb65700c9fbaf6cfa9f10ef62b0931e42a9f Mon Sep 17 00:00:00 2001 From: zamelane Date: Fri, 29 Nov 2024 22:25:45 +0700 Subject: [PATCH 10/12] =?UTF-8?q?=D0=9A=D0=B0=D0=BA=D0=B8=D0=B5-=D1=82?= =?UTF-8?q?=D0=BE=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8,=20=D1=87=D1=82?= =?UTF-8?q?=D0=BE=D0=B1=D1=8B=20=D0=B1=D0=BE=D1=82=20=D0=BD=D0=B5=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=81=D0=B0=D0=BB=20(=D0=BD=D1=83=20=D0=B8?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B1=D1=83=D0=B5=D0=BC=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BB=D0=B0=D0=B2=D0=BB=D0=B8=D0=B2=D0=B0=D1=82=D1=8C,=20?= =?UTF-8?q?=D1=87=D1=82=D0=BE=20=D0=BF=D1=80=D0=BE=D0=B8=D1=81=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D1=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../worker/notificator/bot/botLogic/telegramBot.ts | 14 ++++++++++---- .../bot/notificationLogic/sendMarkEvent.ts | 13 +++++++++---- apps/api/src/worker/notificator/index.ts | 9 ++++++++- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts b/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts index 24b46f04..0b735292 100644 --- a/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts +++ b/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts @@ -1,15 +1,21 @@ import { BOT_TOKEN } from '@config' +import { Telegram } from 'puregram' import { registerLogic } from './registerLogic' -import {Telegram} from "puregram"; const token = BOT_TOKEN export const bot = Bun.main.includes('main.ts') || token === 'IGNORE' ? null - : Telegram.fromToken(token) + : Telegram.fromToken(token, { + apiRetryLimit: -1 + }) if (bot) { - registerLogic(bot) - bot.updates.startPolling() + bot.onError((err) => { + console.error('ОЧЕНЬ СТРАШНАЯ ОШИБКА В PUREGRAM:', err) + return err + }) + registerLogic(bot) + bot.updates.startPolling() } diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts b/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts index 4c11c0b9..e5cf6c9e 100644 --- a/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts +++ b/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts @@ -1,3 +1,4 @@ +import type { ApiResponseError } from 'puregram/lib/types/interfaces' import { buildMessageByEvent } from '../../messages' import type { MarkEvent } from '../../types/MarkEvent' import { bot } from '../botLogic' @@ -17,8 +18,12 @@ export const sendMarkEvent = async (event: MarkEvent) => { if (!bot || !message) return - bot.api.sendMessage({ - chat_id: String(subscribe.tgId), - text: message - }) + const res = (await bot.api.sendMessage({ + chat_id: String(subscribe.tgId), + text: message, + suppress: true + })) as ApiResponseError + + if (!res) + console.error('[PUREGRAM] => error send message =>', JSON.stringify(res)) } diff --git a/apps/api/src/worker/notificator/index.ts b/apps/api/src/worker/notificator/index.ts index 192543d9..b82d0a9e 100644 --- a/apps/api/src/worker/notificator/index.ts +++ b/apps/api/src/worker/notificator/index.ts @@ -35,7 +35,13 @@ export const notificatorChecker = async (): Promise => { if (!subscribe.preActionsIsSuccess) { getCurrPerformance(cacheData, false).then(() => { - if (!bot) return + log( + `Извлёк первичные оценки для ${cacheData.localUserId} юзера ${cacheData.firstName} ${cacheData.lastName} ${cacheData.middleName}` + ) + if (!bot) { + log('С ботом что-то не так, не отправляю сообщение о готовности') + return + } subscribe.preActionsIsSuccess = true subscribe.save() bot.api.sendMessage({ @@ -46,6 +52,7 @@ export const notificatorChecker = async (): Promise => { continue } + log(`Проверяю обновления для ${cacheData.localUserId}`) await getCurrPerformance(cacheData, true) } log(`Проход завершён. Следующий через ${INTERVAL_RUN} секунд`) From d34f422342538d8bb0b006212d629c780eaf1e40 Mon Sep 17 00:00:00 2001 From: zamelane Date: Fri, 29 Nov 2024 23:04:02 +0700 Subject: [PATCH 11/12] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=81=D0=BF=D0=BE=D1=81=D0=BE=D0=B1=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=B2=D0=BE?= =?UTF-8?q?=D1=80=D0=BA=D0=B5=D1=80=D0=B0=20=D0=92=D1=8B=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D1=8C=20checkAll=20=D0=B8=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D1=82=D1=8C,=20=D1=87=D1=82?= =?UTF-8?q?=D0=BE=20=D0=B2=D1=8B=D0=B4=D0=B0=D0=BB=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/helpers/workerDetector.ts | 5 ----- apps/api/src/routes/organization/handler.ts | 7 +++--- .../notificator/bot/botLogic/registerLogic.ts | 10 +++------ .../notificator/bot/botLogic/telegramBot.ts | 21 ++++++++++++------ bun.lockb | Bin 153064 -> 153064 bytes 5 files changed, 21 insertions(+), 22 deletions(-) delete mode 100644 apps/api/src/helpers/workerDetector.ts diff --git a/apps/api/src/helpers/workerDetector.ts b/apps/api/src/helpers/workerDetector.ts deleted file mode 100644 index 872be68d..00000000 --- a/apps/api/src/helpers/workerDetector.ts +++ /dev/null @@ -1,5 +0,0 @@ -let isWorker = false - -export const setIsWorker = (value: boolean) => (isWorker = value) - -export const getIsWorker = () => isWorker diff --git a/apps/api/src/routes/organization/handler.ts b/apps/api/src/routes/organization/handler.ts index 36be6249..d8c32e23 100755 --- a/apps/api/src/routes/organization/handler.ts +++ b/apps/api/src/routes/organization/handler.ts @@ -2,6 +2,7 @@ import type { Optional } from 'sequelize' import { API_CODES, API_ERRORS, ApiError } from '@api' import { SERVER_URL } from '@config' +import type { Organization } from '@diary-spo/shared' import { HeadersWithCookie } from '@utils' import { fetcher } from 'src/utils/fetcher' import { DiaryUserModel } from '../../models/DiaryUser' @@ -19,11 +20,11 @@ const getOrganization = async ({ }: Data): Promise> => { const path = `${SERVER_URL}/services/people/organization` - const response = await fetcher - .get(path, { + const response = await ( + await fetcher.get(path, { headers: HeadersWithCookie(cookie) }) - .json() + ).json() if (response) { /* Хотелось бы красиво по убыванию... diff --git a/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts index 4b1755de..5a86403c 100644 --- a/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts +++ b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts @@ -1,10 +1,10 @@ // @ts-ignore import { b64 } from '@diary-spo/crypto' +import type { Telegram } from 'puregram' import { AuthModel } from '../../../../models/Auth' import { DiaryUserModel } from '../../../../models/DiaryUser' import { SubscribeModel } from '../../../../models/Subscribe' import { INTERVAL_RUN } from '../../config' -import {Telegram} from "puregram"; export const registerLogic = (bot: Telegram | null) => { if (!bot) return @@ -32,17 +32,13 @@ export const registerLogic = (bot: Telegram | null) => { try { tokenSecure = atob(command[1]) } catch { - msg.reply( - 'Вы что-то не то шлёте и всё ломаете. В бан захотели?' - ) + msg.reply('Вы что-то не то шлёте и всё ломаете. В бан захотели?') return } const secureTokenParams = tokenSecure.split(':') if (secureTokenParams.length !== 2 && !Number(secureTokenParams[0])) { - msg.reply( - 'У вашего токена неверная структура. В бан захотел(-а)?' - ) + msg.reply('У вашего токена неверная структура. В бан захотел(-а)?') return } diff --git a/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts b/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts index 0b735292..180bda4f 100644 --- a/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts +++ b/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts @@ -4,18 +4,25 @@ import { registerLogic } from './registerLogic' const token = BOT_TOKEN -export const bot = - Bun.main.includes('main.ts') || token === 'IGNORE' - ? null - : Telegram.fromToken(token, { - apiRetryLimit: -1 - }) +let bot = null -if (bot) { +// Подключаем бота, только если мы в воркере +if (Bun.main.includes('worker') && token !== 'IGNORE') { + bot = Telegram.fromToken(token, { + apiRetryLimit: -1 + }) + + // Отлавливаем ошибки, если таковые имеются bot.onError((err) => { console.error('ОЧЕНЬ СТРАШНАЯ ОШИБКА В PUREGRAM:', err) return err }) + + // Регистрируем команды к боту, на которые отвечаем registerLogic(bot) + + // Слушаем входящие сообщения bot.updates.startPolling() } + +export { bot } diff --git a/bun.lockb b/bun.lockb index 1954b03a3780ae2e1e1f3e308897aa00b9611713..96daa7206c52371e8eb1cf48a7593529c1b380db 100755 GIT binary patch delta 26 icmaE{n)AhK&W0_Fw&yt*v_h-Z2*BO2zdYi From f0bfd32ceb9c8c5b33de72bf8062f966f035bff7 Mon Sep 17 00:00:00 2001 From: zamelane Date: Sun, 15 Dec 2024 22:58:01 +0700 Subject: [PATCH 12/12] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=20=D0=BD=D0=B0=20mtcute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/.env.example | 2 + apps/api/package.json | 4 +- apps/api/src/config/index.ts | 4 +- apps/api/src/config/params.ts | 4 +- apps/api/src/config/types.ts | 2 + .../notificator/bot/botLogic/registerLogic.ts | 39 +++++++++--------- .../notificator/bot/botLogic/telegramBot.ts | 28 +++++++------ .../bot/notificationLogic/sendMarkEvent.ts | 9 +--- apps/api/src/worker/notificator/index.ts | 7 +--- bun.lockb | Bin 153064 -> 156424 bytes 10 files changed, 51 insertions(+), 48 deletions(-) diff --git a/apps/api/.env.example b/apps/api/.env.example index 89deea7d..fcdf7864 100755 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -13,6 +13,8 @@ DATABASE_NAME='databaseName' DATABASE_USERNAME='user' DATABASE_PASSWORD='password' BOT_TOKEN='IGNORE' +API_ID=0 # id приложения +API_HASH='' # какой-то хеш от приложения, см. my.telegram.org/apps POSTGRES_USER=postgres POSTGRES_PW=postgres diff --git a/apps/api/package.json b/apps/api/package.json index 1a9f00d1..602dee9a 100755 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -17,7 +17,8 @@ "dependencies": { "@diary-spo/crypto": "workspace:*", "@elysiajs/cors": "^1.1.1", - "@types/node-telegram-bot-api": "^0.64.7", + "@mtcute/bun": "^0.18.0-rc.5", + "@mtcute/dispatcher": "^0.18.0-rc.5", "cache-manager": "^5.7.6", "elysia": "1.1.21", "elysia-compression": "^0.0.7", @@ -26,7 +27,6 @@ "node-rsa": "^1.1.1", "pg": "^8.13.1", "pg-hstore": "^2.3.4", - "puregram": "^2.26.8", "rand-token": "^1.0.1", "sequelize": "^6.37.5", "sequelize-simple-cache": "^1.3.5" diff --git a/apps/api/src/config/index.ts b/apps/api/src/config/index.ts index d01b8414..dd96729d 100755 --- a/apps/api/src/config/index.ts +++ b/apps/api/src/config/index.ts @@ -23,5 +23,7 @@ export const { DATABASE_PASSWORD, TIMEZONE, KEY_SCAN_PATH, - BOT_TOKEN + BOT_TOKEN, + API_ID, + API_HASH } = PARAMS_INIT diff --git a/apps/api/src/config/params.ts b/apps/api/src/config/params.ts index 35d909b9..e8469427 100755 --- a/apps/api/src/config/params.ts +++ b/apps/api/src/config/params.ts @@ -11,5 +11,7 @@ export const PARAMS_INIT: ParamsInit = { DATABASE_PASSWORD: '', TIMEZONE: getTimezone(), KEY_SCAN_PATH: './', - BOT_TOKEN: '' + BOT_TOKEN: '', + API_ID: 0, + API_HASH: '' } diff --git a/apps/api/src/config/types.ts b/apps/api/src/config/types.ts index 6f8d11e1..1d475e09 100755 --- a/apps/api/src/config/types.ts +++ b/apps/api/src/config/types.ts @@ -19,6 +19,7 @@ type StringKeys = keyof { export interface NumericParams { PORT: number DATABASE_PORT: number + API_ID: number } export interface StringParams { @@ -30,6 +31,7 @@ export interface StringParams { TIMEZONE: string KEY_SCAN_PATH: string BOT_TOKEN: string + API_HASH: string } export type ParamsKeys = StringKeys diff --git a/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts index 5a86403c..0c14329e 100644 --- a/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts +++ b/apps/api/src/worker/notificator/bot/botLogic/registerLogic.ts @@ -1,18 +1,21 @@ // @ts-ignore import { b64 } from '@diary-spo/crypto' -import type { Telegram } from 'puregram' import { AuthModel } from '../../../../models/Auth' import { DiaryUserModel } from '../../../../models/DiaryUser' import { SubscribeModel } from '../../../../models/Subscribe' import { INTERVAL_RUN } from '../../config' +import {html, TelegramClient} from "@mtcute/bun"; +import {Dispatcher} from "@mtcute/dispatcher"; -export const registerLogic = (bot: Telegram | null) => { +export const registerLogic = (bot: Dispatcher | null) => { if (!bot) return - bot.updates.on('message', async (msg) => { + bot.onNewMessage(async (msg) => { const chatId = msg.chat.id + console.log(`Входящее сообщение от ${msg.chat.id} (${msg.chat.username}): ${msg.text}`) + if (!msg.text) { - msg.reply('Такое сообщение не поддерживается') + msg.answerText('Такое сообщение не поддерживается') return } @@ -21,7 +24,7 @@ export const registerLogic = (bot: Telegram | null) => { switch (command[0]) { case '/subscribe': { if (command.length < 2) { - msg.reply( + msg.answerText( 'Передайте вторым параметром актуальный токен, чтобы подписаться на уведомления' ) return @@ -32,13 +35,13 @@ export const registerLogic = (bot: Telegram | null) => { try { tokenSecure = atob(command[1]) } catch { - msg.reply('Вы что-то не то шлёте и всё ломаете. В бан захотели?') + msg.answerText('Вы что-то не то шлёте и всё ломаете. В бан захотели?') return } const secureTokenParams = tokenSecure.split(':') if (secureTokenParams.length !== 2 && !Number(secureTokenParams[0])) { - msg.reply('У вашего токена неверная структура. В бан захотел(-а)?') + msg.answerText('У вашего токена неверная структура. В бан захотел(-а)?') return } @@ -49,7 +52,7 @@ export const registerLogic = (bot: Telegram | null) => { }) if (!auth) { - msg.reply('Переданная авторизация не найдена ...') + msg.answerText('Переданная авторизация не найдена ...') return } @@ -61,7 +64,7 @@ export const registerLogic = (bot: Telegram | null) => { const secureToken = await b64(JSON.stringify(tokenObject)) if (secureToken !== secureTokenParams[1]) { - msg.reply( + msg.answerText( `Ваш токен какой-то не такой. Если вы ничего не трогали, то проблема у нас.\nПожалуйста, покажите это сообщение разработчикам.\nDebug info: ${btoa( JSON.stringify({ tokenSecure, @@ -79,7 +82,7 @@ export const registerLogic = (bot: Telegram | null) => { }) if (subscribes.length >= 1) { - msg.reply( + msg.answerText( 'Вы уже подписаны на уведомления. Сначала отпишитесь (/unsubscribe)' ) return @@ -97,9 +100,8 @@ export const registerLogic = (bot: Telegram | null) => { } }) - msg.reply( - `${user?.firstName} ${user?.lastName}! Вы успешно подписались на уведомления.\nПрежде чем Вы начнёте получать уведомления, нам нужно извлечь все ваши оценки (это просиходит примерно каждые ${INTERVAL_RUN} секунд).\nПо окончанию подготовительных процедур, мы уведомим Вас о готовности принимать уведомления.\nСпасибо, что выбираете нас!`, - { parse_mode: 'HTML' } + msg.answerText( + html`${user?.firstName} ${user?.lastName}! Вы успешно подписались на уведомления.\nПрежде чем Вы начнёте получать уведомления, нам нужно извлечь все ваши оценки (это просиходит примерно каждые ${INTERVAL_RUN} секунд).\nПо окончанию подготовительных процедур, мы уведомим Вас о готовности принимать уведомления.\nСпасибо, что выбираете нас!` ) break } @@ -109,16 +111,15 @@ export const registerLogic = (bot: Telegram | null) => { tgId: chatId } }) - msg.reply( + msg.answerText( 'Вы успешно отписались от всех аккаунтов. Можете привязать новый (/subscribe)' ) break default: - msg.reply( - 'Этой команды нету, но есть такие:' + - '\n/subscribe [token] — подписаться на уведомления по токену' + - '\n/unsubscribe — отписаться от уведомлений', - { parse_mode: 'HTML' } + msg.answerText( + html(`Этой команды нету, но есть такие:` + + `
/subscribe [token] — подписаться на уведомления по токену` + + `
/unsubscribe — отписаться от уведомлений`) ) break } diff --git a/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts b/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts index 180bda4f..98ac752b 100644 --- a/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts +++ b/apps/api/src/worker/notificator/bot/botLogic/telegramBot.ts @@ -1,28 +1,30 @@ -import { BOT_TOKEN } from '@config' -import { Telegram } from 'puregram' +import {API_HASH, API_ID, BOT_TOKEN} from '@config' import { registerLogic } from './registerLogic' +import {TelegramClient} from "@mtcute/bun"; +import {Dispatcher} from "@mtcute/dispatcher"; const token = BOT_TOKEN -let bot = null +let bot: null|TelegramClient = null // Подключаем бота, только если мы в воркере if (Bun.main.includes('worker') && token !== 'IGNORE') { - bot = Telegram.fromToken(token, { - apiRetryLimit: -1 + const tg = new TelegramClient({ + apiId: API_ID, + apiHash: API_HASH }) - // Отлавливаем ошибки, если таковые имеются - bot.onError((err) => { - console.error('ОЧЕНЬ СТРАШНАЯ ОШИБКА В PUREGRAM:', err) - return err - }) + const dp = Dispatcher.for(tg) // Регистрируем команды к боту, на которые отвечаем - registerLogic(bot) + registerLogic(dp) + + // Запускаем бота + await tg.start({ + botToken: BOT_TOKEN + }) - // Слушаем входящие сообщения - bot.updates.startPolling() + bot = tg } export { bot } diff --git a/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts b/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts index e5cf6c9e..b599bd10 100644 --- a/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts +++ b/apps/api/src/worker/notificator/bot/notificationLogic/sendMarkEvent.ts @@ -1,4 +1,3 @@ -import type { ApiResponseError } from 'puregram/lib/types/interfaces' import { buildMessageByEvent } from '../../messages' import type { MarkEvent } from '../../types/MarkEvent' import { bot } from '../botLogic' @@ -18,12 +17,8 @@ export const sendMarkEvent = async (event: MarkEvent) => { if (!bot || !message) return - const res = (await bot.api.sendMessage({ - chat_id: String(subscribe.tgId), - text: message, - suppress: true - })) as ApiResponseError + const res = await bot.sendText(Number(subscribe.tgId), message) if (!res) - console.error('[PUREGRAM] => error send message =>', JSON.stringify(res)) + console.error('[TG_BOT] => error send message =>', JSON.stringify(res)) } diff --git a/apps/api/src/worker/notificator/index.ts b/apps/api/src/worker/notificator/index.ts index b82d0a9e..4b53e819 100644 --- a/apps/api/src/worker/notificator/index.ts +++ b/apps/api/src/worker/notificator/index.ts @@ -4,7 +4,7 @@ import { SubscribeModel } from '../../models/Subscribe' import { getCurrPerformance } from '../../routes/performance.current/service' import { checkInterval } from '../utils/checkInterval' import { logger } from '../utils/logger' -import { bot } from './bot' +import {bot} from './bot' import { INTERVAL_RUN } from './config' let lastRunning: Date | null = null @@ -44,10 +44,7 @@ export const notificatorChecker = async (): Promise => { } subscribe.preActionsIsSuccess = true subscribe.save() - bot.api.sendMessage({ - chat_id: String(subscribe.tgId), - text: 'Мы загрузили ваши оценки. Теперь вы будете получать уведомления!' - }) + bot.sendText(Number(subscribe.tgId), 'Мы загрузили ваши оценки. Теперь вы будете получать уведомления!') }) continue } diff --git a/bun.lockb b/bun.lockb index 96daa7206c52371e8eb1cf48a7593529c1b380db..37decb4f2f5677b4a8908559268a6dc9ce05c72f 100755 GIT binary patch delta 10931 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