Добавлено: первая заглушка для старта рефакторинга
This commit is contained in:
parent
d96e8c4689
commit
1ce533325c
|
@ -0,0 +1,4 @@
|
||||||
|
__pycache__
|
||||||
|
config.json
|
||||||
|
logs
|
||||||
|
*.wpr
|
|
@ -0,0 +1,105 @@
|
||||||
|
from database.l9 import L9_DB
|
||||||
|
from database.tg import TG_DB
|
||||||
|
from utils.config import *
|
||||||
|
import telegram
|
||||||
|
from tg.keyboards import Keyboard
|
||||||
|
import logging
|
||||||
|
from logging.handlers import TimedRotatingFileHandler as TRFL
|
||||||
|
|
||||||
|
logger = logging.getLogger('bot')
|
||||||
|
|
||||||
|
|
||||||
|
def initLogger():
|
||||||
|
if not os.path.isdir(f'logs/bot'):
|
||||||
|
os.makedirs(f'logs/bot')
|
||||||
|
|
||||||
|
f_handler = TRFL(f'./logs/bot/log', when='midnight', encoding="utf-8")
|
||||||
|
|
||||||
|
f_format = logging.Formatter(
|
||||||
|
'%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
datefmt='%d-%b-%y %H:%M:%S',
|
||||||
|
)
|
||||||
|
f_handler.setFormatter(f_format)
|
||||||
|
f_handler.setLevel(logging.INFO)
|
||||||
|
logger.addHandler(f_handler)
|
||||||
|
|
||||||
|
c_handler = logging.StreamHandler()
|
||||||
|
c_format = logging.Formatter('%(levelname)s : %(message)s')
|
||||||
|
c_handler.setFormatter(c_format)
|
||||||
|
logger.addHandler(c_handler)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
class Bot:
|
||||||
|
def __init__(self, token: str, db: L9_DB, tg_db: TG_DB, limit=150):
|
||||||
|
self.l9lk = db
|
||||||
|
self.tg_db = tg_db
|
||||||
|
self.tg = telegram.Bot(token)
|
||||||
|
self.limit = limit
|
||||||
|
self.udpate_id = None
|
||||||
|
self.isWork = True
|
||||||
|
|
||||||
|
def start(self, query: telegram.Message):
|
||||||
|
"""Обработка нового пользователя"""
|
||||||
|
|
||||||
|
# Проверка лимита пользователей и обработка лишних
|
||||||
|
count = self.l9lk.countUsers()
|
||||||
|
tgId = query.from_user.id
|
||||||
|
|
||||||
|
if count >= self.limit:
|
||||||
|
self.tg.sendMessage(
|
||||||
|
tgId,
|
||||||
|
(
|
||||||
|
'Бот работает в тестовом режиме, поэтому количество пользователей временно ограничено.\n'
|
||||||
|
'К сожалению, в данный момент лимит превышен, поэтому доступ для вас закрыт 😢'
|
||||||
|
'Попробуйте зайти на следующей неделе, когда лимит будет повышен'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.tg_db.changeTag(tgId, 'add')
|
||||||
|
self.tg.sendMessage(
|
||||||
|
tgId,
|
||||||
|
(
|
||||||
|
'Привет! Я твой новый помощник, который подскажет тебе, какая сейчас пара, '
|
||||||
|
'и будет напоминать о занятиях, чтобы ты ничего не упустил 🤗\n'
|
||||||
|
'Давай знакомиться! Введи свой номер группы (например, 2305 или 2305-240502D)'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def checkMessages(self):
|
||||||
|
"""Проверка и обработка входящих сообщений"""
|
||||||
|
|
||||||
|
updates = self.tg.get_updates(offset=self.udpate_id, timeout=5)
|
||||||
|
for update in updates:
|
||||||
|
self.udpate_id = update.update_id + 1
|
||||||
|
if update.message:
|
||||||
|
query = update.message
|
||||||
|
tag, l9Id, log = self.tg_db.getTag(query)
|
||||||
|
logger.info(log)
|
||||||
|
tgId = query.from_user.id
|
||||||
|
|
||||||
|
if tag == 'not_started':
|
||||||
|
self.start(query)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.tg.sendMessage(
|
||||||
|
tgId,
|
||||||
|
"Ой!",
|
||||||
|
reply_markup=Keyboard.menu(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
initLogger()
|
||||||
|
logger.info("Start bot")
|
||||||
|
|
||||||
|
config = loadJSON("config")
|
||||||
|
l9lk = L9_DB(**config['sql'])
|
||||||
|
tg_db = TG_DB(l9lk)
|
||||||
|
bot = Bot(config['tg']['token'], l9lk, tg_db, config['tg']['limit'])
|
||||||
|
|
||||||
|
logger.info("Bot ready!")
|
||||||
|
|
||||||
|
while bot.isWork:
|
||||||
|
msgs = bot.checkMessages()
|
|
@ -0,0 +1,137 @@
|
||||||
|
from mysql.connector import connect
|
||||||
|
from mysql.connector.cursor_cext import CMySQLCursor
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
"""Модуль для mysql-connector"""
|
||||||
|
|
||||||
|
def __init__(self, host: str, user: str, password: str):
|
||||||
|
"""Подключение к серверу MySQL"""
|
||||||
|
self.database = connect(host=host, user=user, password=password)
|
||||||
|
self.cursor = self.database.cursor()
|
||||||
|
|
||||||
|
def execute(self, query: str, commit=False) -> CMySQLCursor:
|
||||||
|
"""Выполнить SQL запрос
|
||||||
|
Примечание: в целях безопасности функция игнорирует запросы DROP и TRUNCATE
|
||||||
|
|
||||||
|
Args:
|
||||||
|
:query: текст запроса
|
||||||
|
:commit: [optional] сохранить изменения
|
||||||
|
Returns:
|
||||||
|
:cursor: объект курсора
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
query.lower().find("drop") == -1
|
||||||
|
and query.lower().find("truncate") == -1
|
||||||
|
):
|
||||||
|
self.cursor.execute(query)
|
||||||
|
if commit:
|
||||||
|
self.database.commit()
|
||||||
|
return self.cursor
|
||||||
|
|
||||||
|
def executeFile(self, filename: str, commit=False) -> CMySQLCursor:
|
||||||
|
"""Выполнить запрос из .sql файла
|
||||||
|
|
||||||
|
Args:
|
||||||
|
:filename: название файла (без расширения)
|
||||||
|
:commit: [optional] сохранить изменения
|
||||||
|
Returns:
|
||||||
|
:cursor: объект курсора
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(f'database/{filename}.sql', encoding='utf-8') as f:
|
||||||
|
query = f.read()
|
||||||
|
return self.execute(query, commit)
|
||||||
|
|
||||||
|
def initDatabase(self, name: str):
|
||||||
|
"""Создать базу данных, если таковая отсутствует,
|
||||||
|
и переключиться на неё для использования в дальнейших запросах
|
||||||
|
Args:
|
||||||
|
:name: название базы данных
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.execute(f"CREATE DATABASE IF NOT EXISTS {name}")
|
||||||
|
self.execute(f"USE {name}")
|
||||||
|
|
||||||
|
def initTable(self, name: str, head: str):
|
||||||
|
"""Создать таблицу, если таковая отсутствует
|
||||||
|
|
||||||
|
TODO: вырезать эту функцию, поскольку теперь БД инициализирутся
|
||||||
|
из файла
|
||||||
|
|
||||||
|
Args:
|
||||||
|
:name: название таблицы
|
||||||
|
:head: двумерный список, в строках которых описаны столбцы таблицы
|
||||||
|
"""
|
||||||
|
query = f"CREATE TABLE IF NOT EXISTS `{name}` ("
|
||||||
|
query += ", ".join([" ".join(i) for i in head])
|
||||||
|
query += ");"
|
||||||
|
self.execute(query)
|
||||||
|
|
||||||
|
def insert(self, name: str, values: dict):
|
||||||
|
"""Вставить значение в таблицу
|
||||||
|
|
||||||
|
Args:
|
||||||
|
:name: название таблицы
|
||||||
|
:values: словарь их названий столбцов и их значений
|
||||||
|
"""
|
||||||
|
query = f"INSERT IGNORE INTO `{name}` ("
|
||||||
|
query += ", ".join(values) + ") VALUES ("
|
||||||
|
query += (
|
||||||
|
", ".join(
|
||||||
|
[
|
||||||
|
f'"{i}"' if (i != None) else "NULL"
|
||||||
|
for i in values.values()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
+ ");"
|
||||||
|
)
|
||||||
|
self.execute(query, commit=True)
|
||||||
|
|
||||||
|
def get(self, name: str, condition=None, columns=None) -> list:
|
||||||
|
"""Получить данные из таблицу по запросу вида:
|
||||||
|
|
||||||
|
:SELECT columns FROM name WHERE condition:
|
||||||
|
|
||||||
|
Args:
|
||||||
|
:name: название таблицы
|
||||||
|
:condition: SQL условие для выборки, для получения всех строк оставить None
|
||||||
|
:columns: [optional] список столбцов, которые необходимо выдать, для всех столбцов оставить None
|
||||||
|
"""
|
||||||
|
query = "SELECT " + (', '.join(columns) if columns != None else "*")
|
||||||
|
query += f" FROM `{name}`"
|
||||||
|
query += f" WHERE {condition};" if condition != None else ";"
|
||||||
|
result = self.execute(query).fetchall()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def update(self, name: str, condition: str, new: str):
|
||||||
|
"""Обновить данные в строке
|
||||||
|
|
||||||
|
Args:
|
||||||
|
:name: название таблицы
|
||||||
|
:condition: SQL условие для выборки строки
|
||||||
|
:new: SQL условия для замены значений столбцов
|
||||||
|
"""
|
||||||
|
query = f"UPDATE {name}"
|
||||||
|
query += f" SET {new} WHERE {condition};"
|
||||||
|
self.execute(query, commit=True)
|
||||||
|
|
||||||
|
def newID(self, name: str, id_name: str) -> str:
|
||||||
|
"""Сгенерировать уникальный ID из 9 цифр
|
||||||
|
|
||||||
|
Args:
|
||||||
|
:name: название таблицы пользователей
|
||||||
|
:id_name: название столбца уникальных ID
|
||||||
|
Returns:
|
||||||
|
:someID: строка с уникальным ID
|
||||||
|
"""
|
||||||
|
someID = random.randint(100000000, 999999999)
|
||||||
|
|
||||||
|
result = self.get(name, f"{id_name} = {someID}")
|
||||||
|
|
||||||
|
exist = result != []
|
||||||
|
if not exist:
|
||||||
|
return str(someID)
|
||||||
|
else:
|
||||||
|
self.newID()
|
|
@ -0,0 +1,30 @@
|
||||||
|
CREATE DATABASE IF NOT EXISTS `l9_db`;
|
||||||
|
USE `l9_db`;
|
||||||
|
|
||||||
|
-- Пользователи системы
|
||||||
|
CREATE TABLE IF NOT EXISTS `users` (
|
||||||
|
`l9Id` bigint NOT NULL,
|
||||||
|
-- Идентификатор пользователя системы
|
||||||
|
|
||||||
|
PRIMARY KEY (`l9Id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Сведения о пользователях бота Telegram
|
||||||
|
CREATE TABLE IF NOT EXISTS `tg_users` (
|
||||||
|
`l9Id` bigint NOT NULL,
|
||||||
|
-- Идентификатор пользователя системы
|
||||||
|
|
||||||
|
`tgId` bigint NOT NULL,
|
||||||
|
-- ID пользователя в Telegram
|
||||||
|
|
||||||
|
`name` TEXT,
|
||||||
|
-- (optional) Имя пользователя в Telegram
|
||||||
|
|
||||||
|
`posTag` varchar(30) DEFAULT 'not_started',
|
||||||
|
-- Позиция пользователя в диалоге с ботом:
|
||||||
|
-- (default) not_started - не вступил в диалог
|
||||||
|
-- started - вступил в диалог
|
||||||
|
|
||||||
|
PRIMARY KEY (`l9Id`),
|
||||||
|
CONSTRAINT `l9_tg` FOREIGN KEY (`l9Id`) REFERENCES `users` (`l9Id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
|
@ -0,0 +1,26 @@
|
||||||
|
from .asql import Database
|
||||||
|
|
||||||
|
|
||||||
|
class L9_DB:
|
||||||
|
"""Класс взаимодействия с базой пользователей L9
|
||||||
|
(перспектива для сайта)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, user, password):
|
||||||
|
self.db = Database('localhost', user, password)
|
||||||
|
self.db.initDatabase('l9_db')
|
||||||
|
self.db.executeFile('l9')
|
||||||
|
|
||||||
|
def countUsers(self) -> int:
|
||||||
|
return len(self.db.get('users', None, ['l9Id']))
|
||||||
|
|
||||||
|
def initUser(self, uid):
|
||||||
|
result = self.db.get('users', f"l9Id = {uid}", ["l9Id"])
|
||||||
|
if result == []:
|
||||||
|
l9Id = self.db.newID('users', "l9Id")
|
||||||
|
user = {"l9Id": l9Id}
|
||||||
|
self.db.insert('users', user)
|
||||||
|
else:
|
||||||
|
l9Id = result[0][0]
|
||||||
|
|
||||||
|
return l9Id
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Пользователи системы
|
||||||
|
CREATE TABLE IF NOT EXISTS `users` (
|
||||||
|
`l9Id` bigint NOT NULL,
|
||||||
|
-- Идентификатор пользователя системы
|
||||||
|
|
||||||
|
PRIMARY KEY (`l9Id`)
|
||||||
|
);
|
|
@ -0,0 +1,33 @@
|
||||||
|
from .l9 import L9_DB
|
||||||
|
import telegram
|
||||||
|
|
||||||
|
|
||||||
|
class TG_DB:
|
||||||
|
"""Класс взаимодействия с БД пользователей бота в Telegram"""
|
||||||
|
|
||||||
|
def __init__(self, l9lk: L9_DB):
|
||||||
|
self.l9lk = l9lk
|
||||||
|
self.db = l9lk.db
|
||||||
|
self.db.executeFile('tg')
|
||||||
|
|
||||||
|
def getTag(self, query: telegram.Message) -> (str, str, str):
|
||||||
|
"""Получить тэг и l9Id пользователя"""
|
||||||
|
|
||||||
|
tgId = query.from_user.id
|
||||||
|
name = f'{query.from_user.first_name or ""} {query.from_user.last_name or ""}'
|
||||||
|
|
||||||
|
l9Id = self.db.get('tg_users', f"tgId = {tgId}", ["l9Id"])
|
||||||
|
if l9Id == []:
|
||||||
|
l9Id = self.l9lk.initUser(0)
|
||||||
|
user = {"l9Id": l9Id, "tgId": tgId, "name": name}
|
||||||
|
self.db.insert('tg_users', user)
|
||||||
|
else:
|
||||||
|
l9Id = l9Id[0][0]
|
||||||
|
|
||||||
|
tag = self.db.get('tg_users', f"tgId = {tgId}", ["posTag"])[0][0]
|
||||||
|
|
||||||
|
return tag, l9Id, f'{tgId}\t{tag}\t{name}\t{query.text}'
|
||||||
|
|
||||||
|
def changeTag(self, tgId: int, tag: str) -> None:
|
||||||
|
"""Сменить тэг пользователя"""
|
||||||
|
self.db.update('tg_users', f"tgId = {tgId}", f"posTag = '{tag}'")
|
|
@ -0,0 +1,19 @@
|
||||||
|
-- Сведения о пользователях бота Telegram
|
||||||
|
CREATE TABLE IF NOT EXISTS `tg_users` (
|
||||||
|
`l9Id` bigint NOT NULL,
|
||||||
|
-- Идентификатор пользователя системы
|
||||||
|
|
||||||
|
`tgId` bigint NOT NULL,
|
||||||
|
-- ID пользователя в Telegram
|
||||||
|
|
||||||
|
`name` TEXT,
|
||||||
|
-- (optional) Имя пользователя в Telegram
|
||||||
|
|
||||||
|
`posTag` varchar(30) DEFAULT 'not_started',
|
||||||
|
-- Позиция пользователя в диалоге с ботом:
|
||||||
|
-- (default) not_started - только что в диалог
|
||||||
|
-- add - добавляет группу
|
||||||
|
|
||||||
|
PRIMARY KEY (`l9Id`),
|
||||||
|
CONSTRAINT `l9_tg` FOREIGN KEY (`l9Id`) REFERENCES `users` (`l9Id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
|
@ -0,0 +1,32 @@
|
||||||
|
from telegram import (
|
||||||
|
InlineKeyboardMarkup,
|
||||||
|
InlineKeyboardButton,
|
||||||
|
ReplyKeyboardMarkup,
|
||||||
|
KeyboardButton,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Keyboard:
|
||||||
|
def confirm() -> InlineKeyboardMarkup:
|
||||||
|
"""Клавиатура Да/Нет"""
|
||||||
|
buttons = [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("Да", callback_data="yes"),
|
||||||
|
InlineKeyboardButton("Нет", callback_data="no"),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
return InlineKeyboardMarkup(buttons)
|
||||||
|
|
||||||
|
def cancel() -> ReplyKeyboardMarkup:
|
||||||
|
"""Кнопка отмены"""
|
||||||
|
buttons = [[KeyboardButton("Отмена")]]
|
||||||
|
return ReplyKeyboardMarkup(
|
||||||
|
buttons, resize_keyboard=True, one_time_keyboard=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def menu() -> ReplyKeyboardMarkup:
|
||||||
|
"""Кнопка Главного меню"""
|
||||||
|
buttons = [[KeyboardButton("Главное меню")]]
|
||||||
|
return ReplyKeyboardMarkup(
|
||||||
|
buttons, resize_keyboard=True, one_time_keyboard=True
|
||||||
|
)
|
|
@ -0,0 +1,16 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def loadJSON(name):
|
||||||
|
path = f"{name}.json"
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path, encoding='utf-8') as file:
|
||||||
|
return json.load(file)
|
||||||
|
|
||||||
|
|
||||||
|
def saveJSON(name, dct):
|
||||||
|
path = f"{name}.json"
|
||||||
|
with open(path, "w", encoding='utf-8') as file:
|
||||||
|
json.dump(dct, file, ensure_ascii=False, indent="\t")
|
Reference in New Issue