commit 8060b933a53ea1eb72457c76d754e5b4f569473c Author: Jemacivan <63158240+Jemacivan@users.noreply.github.com> Date: Wed Feb 16 17:13:44 2022 +0200 Init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfe45d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Bot files +*.ini +!example_config.ini +*.json +*.db +*.sqlite3 +*.txt +!requirements.txt +test.py +venv/ diff --git a/backup/google_parser/README.md b/backup/google_parser/README.md new file mode 100644 index 0000000..6f00b03 --- /dev/null +++ b/backup/google_parser/README.md @@ -0,0 +1,38 @@ +# Installing +**Create VirtualEnv** +``` +python -m venv venv +``` +**Activate VirtualEnv** +``` +source venv/bin/activate +``` +**Installing requirements** +``` +pip install -r requirements.txt +``` +**Rename example_config.ini -> config.ini +Enter Telegram Bot API Token on config.ini + +Optional: setup webhook, telegram bot api server, etc. + +Enter DocumentID ([How to get DocumentID](https://developers.google.com/docs/api/how-tos/overview#document_id)) + +[Create project and enable Docs API](https://developers.google.com/workspace/guides/create-project) + +Put credentials.json ([How to get credentials.json](https://developers.google.com/workspace/guides/create-credentials?hl=ru#create_a_oauth_client_id_credential))** + +**Run setup docs parser** +``` +python setup_google_docs_api.py +``` +**Follow Setup Wizard +Open web page +Login your account +If you get error, setup redirect in Project** + +# Running +``` +python bot.py +``` +**Run on docker: Supported!** diff --git a/backup/google_parser/example_config.ini b/backup/google_parser/example_config.ini new file mode 100644 index 0000000..e90c149 --- /dev/null +++ b/backup/google_parser/example_config.ini @@ -0,0 +1,31 @@ +[Docs_Settings] +Document_ID = 123ABC +Config_folder = configs/ +token_file = token.json +credentials_file = credentials.json +data_file = data.json + +[Bot] +token = 123:JAKD +; None = Not used local telegram bot api server +; Example http://127.0.0.1:8888 +telegram_bot_api_server = none +; True or False +use_webhook = false +ip = 127.0.0.1 +port = 3001 +; if you don`t use local TelegramBotAPI Server + WebHooks -> Set settings in bot.py + +[Users] +;Uncomment this variable, if you use filters to users +;allowed_users = 0,1,2,3 +admin_users = 0,1,2,3 + +[DataBase] +enable_logging = yes +database_link = sqlite:///db.sqlite3 + +[announcements] +;Seconds only/ +time = 14400 +;//////////// diff --git a/backup/google_parser/parser/__init__.py b/backup/google_parser/parser/__init__.py new file mode 100644 index 0000000..3188cf5 --- /dev/null +++ b/backup/google_parser/parser/__init__.py @@ -0,0 +1,2 @@ +from .parser import get_about_replacements, docs_parse +__all__ = ['get_about_replacements', 'docs_parse'] diff --git a/backup/google_parser/parser/parser.py b/backup/google_parser/parser/parser.py new file mode 100644 index 0000000..7c728ce --- /dev/null +++ b/backup/google_parser/parser/parser.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os +import json + +from googleapiclient.discovery import build +from google_auth_oauthlib.flow import InstalledAppFlow +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials + +from load import config +from .utils import Helper + +# If modifying these scopes, delete the file token.json. +SCOPES = ['https://www.googleapis.com/auth/documents.readonly'] + +__all__ = ['docs_parse', 'get_about_replacements'] + + +def docs_parse() -> None: + creds = None + # The file token.json stores the user's access and refresh tokens, and is + # created automatically when the authorization flow completes for the first + # time. + if os.path.exists(config.token_file): + creds = Credentials.from_authorized_user_file( + config.token_file, + SCOPES + ) + # If there are no (valid) credentials available, let the user log in. + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file( + config.credentials_file, SCOPES) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open(config.token_file, 'w') as token: + token.write(creds.to_json()) + + service = build('docs', 'v1', credentials=creds) + + # Retrieve the documents contents from the Docs service. + document = service.documents().get(documentId=config.documentid).execute() + if os.path.exists(config.data_file): + os.remove(config.data_file) + + with open(config.data_file, 'w') as f: + json.dump(document, f, ensure_ascii=False) + f.close() + + +def read_parse_data(): + with open(config.data_file, 'r') as f: + data = json.loads(f.read()) + f.close() + return data + + +def get_about_replacements() -> dict: + helper = Helper() + document = read_parse_data() + info = [] + element = helper.get_table_element() + + try: + count = document['body']["content"][element]["table"]["rows"] + except (IndexError, KeyError): + element = helper.find_with_table(document) + if element: + count = document['body']["content"][element]["table"]["rows"] + else: + info = helper.find_with_text(document) + + date = helper.get_date(document) + + another_teacher = helper.teacher(document) + + if element: + for c in range(0, count): + more_replaces = (document['body'] + ["content"][element]["table"] + ["tableRows"][c]["tableCells"][1] + ["content"] + ) + replaces = '' + for i in range(0, len(more_replaces)): + replaces += (document['body']["content"][element]["table"] + ["tableRows"][c]["tableCells"][1] + ["content"][i]["paragraph"]["elements"][0] + ["textRun"]["content"].rstrip("\n")) + + info.append( + ( + document['body']["content"][element]["table"] + ["tableRows"][c]["tableCells"][0] + ["content"][0]["paragraph"]["elements"][0] + ["textRun"]["content"].rstrip("\n"), + replaces + ) + ) + + return { + 'date': date if type(date) != type(False) else "Error" , + 'data': dict(info), + 'another_teacher': another_teacher, + } diff --git a/backup/google_parser/parser/utils.py b/backup/google_parser/parser/utils.py new file mode 100644 index 0000000..ca47ff2 --- /dev/null +++ b/backup/google_parser/parser/utils.py @@ -0,0 +1,188 @@ +import os +import datetime +from datetime import datetime as dt + +from load import config + + +def date_parser_helper(days:int, parse:str="%d.%m.20%y"): + return dt.strftime( + dt.now() + + datetime.timedelta(days=days), + parse + ) + +''' +self.months = { + 1: "січень", + 2: "лютий", + 3: "березень", + 4: "квітень", + 5: "травень", + 6: "червень", + 7: "липень", + 8: "серпень", + 9: "вересень", + 10: "жовтень", + 11: "листопад", + 12: "грудень" +} +''' + +class Helper(): + + def __init__(self): + self.date_now = date_parser_helper(0) + self.date_next = date_parser_helper(1) + self.weekend_pass = date_parser_helper(2) + self.two_day_pass = date_parser_helper(3) + + self.black_list = [ + 'черговий викладач', + self.date_now, + self.date_next, + self.weekend_pass, + self.two_day_pass + ] + + @staticmethod + def find_with_table(document): + c_element = 2 + while True: + try: + document['body']["content"][c_element]["table"]["rows"] + break + except KeyError: + c_element += 1 + if c_element > 15: + return False + except IndexError: + return False + + with open("{}/table_element.txt".format(config.config_folder), 'w') as f: + f.write(str(c_element)) + f.close() + return c_element + + def find_with_text(self, document): + format_charset = '-' + alternative_format_charset = "\t" + element = 4 + data = [] + text = '' + + while element < 15: + doc = ( + document['body']["content"][element] + ["paragraph"]["elements"][0]["textRun"]["content"] + ).rstrip("\n").replace("–", "-", 1) + if ( + ( + ("-" in doc) + #and + #("\t" not in doc) + ) + and + ([p not in doc.lower() for p in self.black_list][0]) + ): + try: + group, text = doc.split(format_charset) + except ValueError: + if element > 6: + break + else: + try: + group, text = doc.split(alternative_format_charset) + except ValueError: + if element > 6: + break + if text != '': + data.append( + (group.strip(" "), text.lstrip(" ").replace("\t", "")) + ) + element += 1 + return data + + def get_date(self, document): + date_element = 1 + while date_element < 16: + try: + date = ( + document['body']["content"][date_element] + ["paragraph"]["elements"][0]["textRun"]["content"] + .rstrip(" \n")) + except: + date_element += 1 + if ( + ( + ( + self.date_now in date.lower() + .lstrip("заміни").lstrip("на").replace(" ", "") + ) + or + ( + self.date_next in date.lower() + .lstrip("заміни").lstrip("на").replace(" ", "") + ) + or + ( + self.weekend_pass in date.lower() + .lstrip("заміни").lstrip("на").replace(" ", "") + ) + or + ( + self.two_day_pass in date.lower() + .lstrip("заміни").lstrip("на").replace(" ", "") + ) + ) + or + ( + "заміни на" in date.lower() + ) + ): + return date + else: + date_element += 1 + + return False + + @staticmethod + def get_table_element(): + if os.path.exists(f"{config.config_folder}/table_element.txt"): + element = int( + open( + f"{config.config_folder}/table_element.txt", + 'r' + ) + .read() + ) + else: + element = 6 + return element + + @staticmethod + def teacher(document): + element = 1 + while element < 6: + if "paragraph" in document['body']["content"][element]: + length_element = (len(document['body']["content"][element] + ["paragraph"]["elements"])) + + doc = ( + document['body']["content"][element]["paragraph"]["elements"] + [0]["textRun"]["content"].rstrip("\n") + ) + if 'черговий викладач' in doc.lower().replace("–", ""): + return doc + + elif length_element > 1: + for p in range(length_element): + doc = ( + document['body']["content"][element] + ["paragraph"]["elements"] + [p]["textRun"]["content"].rstrip("\n") + ) + if 'черговий викладач' in doc.lower().replace("–", ""): + return doc + + element += 1 diff --git a/backup/google_parser/requirements.txt b/backup/google_parser/requirements.txt new file mode 100644 index 0000000..e6d4123 --- /dev/null +++ b/backup/google_parser/requirements.txt @@ -0,0 +1,9 @@ +google-api-python-client +google-auth-httplib2 +google-auth-oauthlib +peewee +aiogram +cryptography +pymysqldb +psycopg2 +aioschedule diff --git a/backup/google_parser/setup_google_docs_api.py b/backup/google_parser/setup_google_docs_api.py new file mode 100644 index 0000000..5a90893 --- /dev/null +++ b/backup/google_parser/setup_google_docs_api.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +''' +Don`t move this file! +''' + +if __name__ == '__main__': + from parser import docs_parse + docs_parse() diff --git a/bot.py b/bot.py new file mode 100755 index 0000000..3ea348d --- /dev/null +++ b/bot.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +import sys +import logging +import asyncio + +from aiogram import executor + +import filters +import handlers +from utils.announcements import scheduler +from load import dp, bot, config +from utils import set_commands + + +if (len(sys.argv) >= 2) and (sys.argv[1] == '-u'): + from parser import docs_parse; docs_parse() + sys.exit(0) + +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger("Bot") + +WEBAPP_HOST = config.bot("ip") +WEBAPP_PORT = config.bot("port") + +WEBHOOK_HOST = f'http://{WEBAPP_HOST}:{WEBAPP_PORT}' +WEBHOOK_PATH = f'/bot{config.bot("token")}/' +WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}" + + +async def on_startup(dp): + await set_commands(dp) + await bot.set_webhook(url=WEBHOOK_URL) + + +async def on_shutdown(dp): + await bot.delete_webhook() + + +def main() -> None: + if config.logging_user: + logger.info("Logging enabled!") + else: + logger.info("Logging disabled!") + + #loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + #loop.create_task(scheduler()) + + if config.bot("use_webhook").lower() in ['t', 'true', '1', 'yes', 'y']: + executor.start_webhook( + dispatcher=dp, + loop=loop, + webhook_path=WEBHOOK_PATH, + on_startup=on_startup, + skip_updates=False, + on_shutdown=on_shutdown, + host=WEBAPP_HOST, + port=WEBAPP_PORT, + ) + else: + executor.start_polling(dp, skip_updates=True) + + +if __name__ == '__main__': + main() diff --git a/configs/__init__.py b/configs/__init__.py new file mode 100644 index 0000000..6e4f6e4 --- /dev/null +++ b/configs/__init__.py @@ -0,0 +1 @@ +from .configure import Configure \ No newline at end of file diff --git a/configs/configure.py b/configs/configure.py new file mode 100644 index 0000000..e97a63f --- /dev/null +++ b/configs/configure.py @@ -0,0 +1,32 @@ +from configparser import ConfigParser + +from .module import Config + + +CONFIG_FILE = 'config.ini' + + +class Configure(Config): + + def __init__(self): + + self.config = ConfigParser() + self.data = dict() + self.__readconfig() + + def __readconfig(self): + self.config.read(CONFIG_FILE) + for section in self.config.sections(): + self.data[section] = dict() + + for (key, value) in self.config.items(section): + self.data[section][key] = value + + def bot(self, key): + return self.data["Bot"][key] + + def db(self, key): + return self.data["DataBase"][key] + + def anons(self, key): + return self.data["announcements"][key] diff --git a/configs/module.py b/configs/module.py new file mode 100644 index 0000000..ae18fab --- /dev/null +++ b/configs/module.py @@ -0,0 +1,49 @@ +class Config(): + + @property + def config_folder(self): + return self.config.get("Docs_Settings", "Config_folder").rstrip("/") + + @property + def documentid(self): + return self.config.get("Docs_Settings", 'Document_ID') + + @property + def token_file(self): + file = self.config.get("Docs_Settings", "token_file") + return (self.config_folder + "/" + file) + + @property + def data_file(self): + file = self.config.get("Docs_Settings", "data_file") + return (self.config_folder + "/" + file) + + @property + def credentials_file(self): + file = self.config.get("Docs_Settings", "credentials_file") + return (self.config_folder + "/" + file) + + @property + def allowed_users(self): + usrs = self.config.get("Users", "allowed_users").split(',') + return [int(user_id) for user_id in usrs] + + @property + def admin_user(self): + usrs = self.config.get("Users", "admin_users").split(',') + return [int(user_id) for user_id in usrs] + + @property + def telegram_bot_api_server(self): + server = self.config.get("Bot", "telegram_bot_api_server") + if str(server).lower() == "none": + return "https://api.telegram.org" + else: + return server + + @property + def logging_user(self): + o = self.config.get("DataBase", "enable_logging") + if o.lower() in ['t', "yes", "true"]: + return True + return False diff --git a/database/__init__.py b/database/__init__.py new file mode 100644 index 0000000..6f9ec0d --- /dev/null +++ b/database/__init__.py @@ -0,0 +1,2 @@ +__all__ = ["register", "get_all_users"] +from .worker import register, get_all_users, set_group_settings, get_group diff --git a/database/models.py b/database/models.py new file mode 100644 index 0000000..aecb657 --- /dev/null +++ b/database/models.py @@ -0,0 +1,26 @@ +from datetime import datetime + +from peewee import Model, BigIntegerField, CharField, DateTimeField + +from load import db + + + +class Users(Model): + user_id = BigIntegerField(null=False) + first_name = CharField(null=True) + last_name = CharField(null=True) + username = CharField(null=True) + date = DateTimeField(default=datetime.now) + + class Meta: + database = db + + +class Chat(Model): + chat_id = BigIntegerField(null=False) + group = CharField(null=False) + + class Meta: + database = db + diff --git a/database/worker.py b/database/worker.py new file mode 100644 index 0000000..fa8a2f6 --- /dev/null +++ b/database/worker.py @@ -0,0 +1,52 @@ +from typing import Union, List + +from .models import Users, Chat, db + + +db.create_tables([Users, Chat]) + + +def register( + user_id: int, + username: Union[str, None], + first_name: str, + last_name: Union[str, None] +) -> None: + if not Users.select().where(Users.user_id == user_id).exists(): + Users.create( + user_id=user_id, + username=username, + first_name=first_name, + last_name=last_name + ) + else: + (Users.update( + username=username, + first_name=first_name, + last_name=last_name + ) + .where(Users.user_id == user_id).execute()) + + +def get_all_users() -> List[int]: + usr = [] + for user in Users.select(): + usr.append(user.user_id) + return usr + + +def set_group_settings(chat_id: int, group: Union[int, str]) -> None: + if Chat.select().where(Chat.chat_id == chat_id).exists(): + Chat.update(group=group).where(Chat.chat_id == chat_id).execute() + else: + Chat.create( + chat_id=chat_id, + group=group + ) + + +def get_group(chat_id: int) -> Union[str, None]: + if Chat.select().where(Chat.chat_id == chat_id).exists(): + return Chat.get(Chat.chat_id == chat_id).group + return None + diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..2dda1af --- /dev/null +++ b/dockerfile @@ -0,0 +1,16 @@ +FROM alpine:latest + +COPY . /usr/src/bot +WORKDIR /usr/src/bot + +RUN apk update \ + && apk add \ + build-base \ + gcc \ + musl-dev \ + python3-dev \ + py3-pip \ + postgresql-dev + +RUN pip install --no-cache-dir -r requirements.txt +CMD ["python3", "bot.py"] diff --git a/engineering_works.py b/engineering_works.py new file mode 100644 index 0000000..d3aac5f --- /dev/null +++ b/engineering_works.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +import logging + +from aiogram import executor, types + +from load import bot, dp, config +from database import get_all_users + +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) + + + +WEBAPP_HOST = config.bot("ip") +WEBAPP_PORT = config.bot("port") + +WEBHOOK_HOST = f'http://{WEBAPP_HOST}:{WEBAPP_PORT}' +WEBHOOK_PATH = f'/bot{config.bot("token")}/' +WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}" + +engeneerings_works = ( + "Техничиские работы..." + "Постараемся восстановить работу как можно раньше!" +) + +parse_error = ( + "Бот приостановлен на неопределенный срок!\n" + "Что случилось?\n" + "Администрация коледжа изменила формат файла с google docs на docx(Microsoft Office)\n" + "Замены вы можете посмотреть тут: https://docs.google.com/document/d/{}".format(config.documentid) +) + +new_year = ( + "С новым годом!❄️\n" + "Бот будет отключён до 16.01.2022(Период зимних каникул)\n" +) + +link_replace = 'https://tfk.org.ua/zamini-do-rozkladu-08-51-30-03-02-2022/' + +the_end =( + "Всё было восстановлено и настроено. Бот продолжает работу!:)" +) + + +send_msg = the_end + +async def on_startup(dp): + await bot.set_webhook(url=WEBHOOK_URL) + + +async def on_shutdown(dp): + await bot.delete_webhook() + +@dp.message_handler(commands=['send']) +async def asd(message): + for user_id in get_all_users(): + if user_id != 1083440854: + print(user_id) + try: + await bot.send_message(chat_id=user_id, text=send_msg) + except: + pass + +#@dp.message_handler() +async def start(message: types.Message): + logging.info( + "{} - {}".format( + message.from_user.id, + message.from_user.username + ) + ) + await bot.send_message( + message.chat.id, + engeneerings_works + ) + +if __name__ == "__main__": + if config.bot("use_webhook").lower() in ['t', 'true', '1', 'yes', 'y']: + executor.start_webhook( + dispatcher=dp, + webhook_path=WEBHOOK_PATH, + on_startup=on_startup, + skip_updates=True, + on_shutdown=on_shutdown, + host=WEBAPP_HOST, + port=WEBAPP_PORT, + ) + else: + executor.start_polling(dp, skip_updates=True) diff --git a/example_config.ini b/example_config.ini new file mode 100644 index 0000000..e90c149 --- /dev/null +++ b/example_config.ini @@ -0,0 +1,31 @@ +[Docs_Settings] +Document_ID = 123ABC +Config_folder = configs/ +token_file = token.json +credentials_file = credentials.json +data_file = data.json + +[Bot] +token = 123:JAKD +; None = Not used local telegram bot api server +; Example http://127.0.0.1:8888 +telegram_bot_api_server = none +; True or False +use_webhook = false +ip = 127.0.0.1 +port = 3001 +; if you don`t use local TelegramBotAPI Server + WebHooks -> Set settings in bot.py + +[Users] +;Uncomment this variable, if you use filters to users +;allowed_users = 0,1,2,3 +admin_users = 0,1,2,3 + +[DataBase] +enable_logging = yes +database_link = sqlite:///db.sqlite3 + +[announcements] +;Seconds only/ +time = 14400 +;//////////// diff --git a/filters/__init__.py b/filters/__init__.py new file mode 100644 index 0000000..790afa8 --- /dev/null +++ b/filters/__init__.py @@ -0,0 +1,7 @@ +from load import dp +from .main import BotAdmin #OnlyMy + + +if __name__ == "filters": + dp.filters_factory.bind(BotAdmin) + #dp.filters_factory.bind(OnlyMy) diff --git a/filters/main.py b/filters/main.py new file mode 100644 index 0000000..849e6de --- /dev/null +++ b/filters/main.py @@ -0,0 +1,36 @@ +from aiogram import types +from aiogram.dispatcher.filters import BoundFilter + +from load import config + +''' +class OnlyMy(BoundFilter): + key = 'only_my' + + def __init__(self, only_my): + self.onlymy = only_my + + async def check(self, message: types.Message): + logging.info("User: {user_id} - {username}".format( + user_id=str(message.from_user.id), + username=str(message.from_user.username) + )) + if message.from_user.id in config.allowed_users: + return True + return False +''' + + +class BotAdmin(BoundFilter): + key = 'admin' + + def __init__(self, admin): + self.admin = admin + + async def check(self, message: types.Message): + if message.from_user.id in config.admin_user: + return True + else: + await message.answer("Хорошая попытка, но ты не администратор!") + return False + diff --git a/handlers/__init__.py b/handlers/__init__.py new file mode 100644 index 0000000..0958047 --- /dev/null +++ b/handlers/__init__.py @@ -0,0 +1,4 @@ +from . import groups +from . import private +from . import callback +from . import errors diff --git a/handlers/callback/__init__.py b/handlers/callback/__init__.py new file mode 100644 index 0000000..deec4a8 --- /dev/null +++ b/handlers/callback/__init__.py @@ -0,0 +1 @@ +from . import main \ No newline at end of file diff --git a/handlers/callback/main.py b/handlers/callback/main.py new file mode 100644 index 0000000..bcd66a3 --- /dev/null +++ b/handlers/callback/main.py @@ -0,0 +1,56 @@ +import logging + +from aiogram import types + +from load import dp +from keyboards.inline.keyboard import cancel_button, menu +from parser import get_about_replacements + + +@dp.callback_query_handler(lambda c: c.data != "back") +async def callback_query(query: types.CallbackQuery): + from_user = query.from_user + data = get_about_replacements() + group = query.data + + logging.info("Button: {btn}, User: {user_id} - {username}".format( + user_id=str(from_user.id), + username=str(from_user.username), + btn=str(group) + )) + + if group in data['data']: + await query.message.edit_text( + text="Группа: {group}\nЗамены: {replace}" + .format( + group=str(group), + replace=data['data'][group] + ), + reply_markup=cancel_button + ) + else: + await query.message.edit_text( + text=( + "Группа: {group} не найдена!\n" + "Список обновится автоматически после нажатия кнопки ниже" + ) + .format( + group=str(group), + ), + reply_markup=cancel_button + ) + #await query.answer() + + +@dp.callback_query_handler(lambda c: c.data == "back") +async def back_button(query: types.CallbackQuery): + data = get_about_replacements() + await query.message.edit_text( + "{date}\n{teacher}\nВыберите свою группу" + .format( + date=data["date"], + teacher=data["another_teacher"] + ), + reply_markup=menu(data["data"]) + ) + await query.answer() diff --git a/handlers/errors/__init__.py b/handlers/errors/__init__.py new file mode 100644 index 0000000..deec4a8 --- /dev/null +++ b/handlers/errors/__init__.py @@ -0,0 +1 @@ +from . import main \ No newline at end of file diff --git a/handlers/errors/main.py b/handlers/errors/main.py new file mode 100644 index 0000000..a77c4cc --- /dev/null +++ b/handlers/errors/main.py @@ -0,0 +1,11 @@ +import logging +from aiogram.utils.exceptions import BotBlocked + +from load import dp + + +@dp.errors_handler() +async def errors_handler(update, exception): + if isinstance(exception, BotBlocked): + logging.info("Bot blocked") + return True \ No newline at end of file diff --git a/handlers/groups/__init__.py b/handlers/groups/__init__.py new file mode 100644 index 0000000..deec4a8 --- /dev/null +++ b/handlers/groups/__init__.py @@ -0,0 +1 @@ +from . import main \ No newline at end of file diff --git a/handlers/groups/main.py b/handlers/groups/main.py new file mode 100644 index 0000000..a8a1dce --- /dev/null +++ b/handlers/groups/main.py @@ -0,0 +1,64 @@ +import logging + +from aiogram import types +from aiogram.dispatcher.filters import ChatTypeFilter + +from load import dp, bot, config +from database import set_group_settings, get_group +from parser import get_about_replacements +from database import register + + +@dp.message_handler(ChatTypeFilter(['group', 'supergroup']), commands=['set']) +async def set_group(message: types.Message): + if (message.from_user.id not in [admin.user.id for admin in await bot.get_chat_administrators(message.chat.id)]) and (message.from_user.id not in config.admin_user): + await message.answer("Вы не являетесь администратором чата!") + return + args = message.text.split() + if len(args) < 2: + await message.answer( + ("Вы не передали имя своей группы!\n" + "Пример: /set 221") + ) + return + + set_group_settings(message.chat.id, args[1]) + await message.answer("Настройка завершена успешно!") + +@dp.message_handler(ChatTypeFilter(['group', 'supergroup']), commands=['start', 'get']) +async def get_replace_on_chat(message: types.Message): + if config.logging_user: + register( + user_id=message.from_user.id, + username=message.from_user.username, + first_name=str(message.from_user.first_name), + last_name=message.from_user.last_name + ) + + logging.info("User: {user_id} - {username}".format( + user_id=str(message.from_user.id), + username=str(message.from_user.username) + )) + + data = get_about_replacements() + group = get_group(message.chat.id) + + if group is not None: + if group in data['data']: + await message.answer( + ( + "Группа: {group}\n" + "Замены {date}\n" + "{teacher}\n" + "Замены: {replace}\n" + ).format( + group=str(group), + replace=data['data'][group], + date=data["date"].lower(), + teacher=data["another_teacher"] + ) + ) + else: + await message.answer("Похоже замен нет") + else: + await message.answer("Похоже администратор группы не настроил привязку") diff --git a/handlers/private/__init__.py b/handlers/private/__init__.py new file mode 100644 index 0000000..9ea4ffa --- /dev/null +++ b/handlers/private/__init__.py @@ -0,0 +1,2 @@ +from . import main +from . import admin \ No newline at end of file diff --git a/handlers/private/admin.py b/handlers/private/admin.py new file mode 100644 index 0000000..23b9add --- /dev/null +++ b/handlers/private/admin.py @@ -0,0 +1,23 @@ +import logging +from aiogram import types + +from load import dp, bot +from parser import docs_parse + + +@dp.message_handler(admin=True, commands=['reload']) +async def refresh(message: types.Message): + m = await bot.send_message( + message.chat.id, + "Идёт обновление информации..." + ) + try: + docs_parse() + await m.edit_text( + "Информация о заменах была обновлена!" + ) + except Exception as e: + logging.error(e) + await m.edit_text( + "Произойшла ошибка!" + ) diff --git a/handlers/private/main.py b/handlers/private/main.py new file mode 100644 index 0000000..95f9d5b --- /dev/null +++ b/handlers/private/main.py @@ -0,0 +1,82 @@ +import logging + +from aiogram import types +from aiogram.dispatcher.filters import ChatTypeFilter + +from load import dp, bot, config +from keyboards.inline.keyboard import menu +from parser import get_about_replacements +if config.logging_user: + from database import register + + +@dp.message_handler(commands=["help"]) +async def help_msg(message: types.Message): + await bot.send_message( + message.chat.id, + ( + "Я всего-лишь небольшой помощник:3\n" + "Умею работать в чатах, для настройки попросите администратора чата указать группу с помощью команды /set\n" + "/set - Установить группу, для получения данных о заменах(Работает ТОЛЬКО в чатах)\n" + "/start /get - получить информацию о заменах\n" + ) + ) + + +@dp.message_handler(ChatTypeFilter(['private']), commands=['start', 'get']) +async def get_replace(message: types.Message): + if config.logging_user: + register( + user_id=message.from_user.id, + username=message.from_user.username, + first_name=str(message.from_user.first_name), + last_name=message.from_user.last_name + ) + + link = ( + 'Проверьте замены тут' + .format(config.bot("link")) + ) + logging.info("User: {user_id} - {username}".format( + user_id=str(message.from_user.id), + username=str(message.from_user.username) + )) + + try: + data = get_about_replacements() + await bot.send_message( + message.chat.id, + "Замены {date}\n{teacher}\nВыберите свою группу" + .format( + date=data["date"].lower(), + teacher=str(data["another_teacher"]).title() + ), + reply_markup=menu(data["data"]) + ) + + except Exception as e: + logging.error(str(e)) + err_msg = ( + "Техничиские шоколадки... " + f"Скорее всего структура файла была изменена\n{link}" + ) + await bot.send_message( + message.chat.id, + err_msg, + parse_mode='HTML', + disable_web_page_preview=True + ) + + +@dp.message_handler(commands=['link']) +async def get_link(message: types.Message): + msg = ( + 'Проверьте замены тут' + .format(config.bot("link")) + ) + await bot.send_message( + message.chat.id, + msg, + parse_mode='HTML', + disable_web_page_preview=True + ) diff --git a/keyboards/__init__.py b/keyboards/__init__.py new file mode 100644 index 0000000..a1df4b8 --- /dev/null +++ b/keyboards/__init__.py @@ -0,0 +1,2 @@ +from . import inline +from . import default diff --git a/keyboards/default/__init__.py b/keyboards/default/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/keyboards/inline/__init__.py b/keyboards/inline/__init__.py new file mode 100644 index 0000000..fd8ef9b --- /dev/null +++ b/keyboards/inline/__init__.py @@ -0,0 +1 @@ +from . import keyboard diff --git a/keyboards/inline/keyboard.py b/keyboards/inline/keyboard.py new file mode 100644 index 0000000..518888b --- /dev/null +++ b/keyboards/inline/keyboard.py @@ -0,0 +1,21 @@ +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton + + +def menu(data: dict) -> InlineKeyboardMarkup: + markup = InlineKeyboardMarkup() + for k in data: + if k.replace(" ", "") != "": + markup.add( + InlineKeyboardButton( + k, callback_data=str(k) + ) + ) + return markup + + +cancel_button = InlineKeyboardMarkup(inline_keyboard=[ + [ + InlineKeyboardButton("Назад", callback_data="back") + ] + ] +) diff --git a/load.py b/load.py new file mode 100644 index 0000000..6085b4d --- /dev/null +++ b/load.py @@ -0,0 +1,16 @@ +from aiogram import Dispatcher, Bot +from aiogram.bot.api import TelegramAPIServer +from playhouse.db_url import connect + +from configs import Configure + + +config = Configure() + +db = connect(config.db("db_link")) + +bot = Bot( + token=config.bot("token"), + server=TelegramAPIServer.from_base(config.telegram_bot_api_server) +) +dp = Dispatcher(bot) diff --git a/parser/__init__.py b/parser/__init__.py new file mode 100644 index 0000000..3188cf5 --- /dev/null +++ b/parser/__init__.py @@ -0,0 +1,2 @@ +from .parser import get_about_replacements, docs_parse +__all__ = ['get_about_replacements', 'docs_parse'] diff --git a/parser/parser.py b/parser/parser.py new file mode 100644 index 0000000..6bf3fa8 --- /dev/null +++ b/parser/parser.py @@ -0,0 +1,63 @@ +import requests +import json +import datetime +from datetime import datetime as dt + +from bs4 import BeautifulSoup + +try: + from load import config +except: config = None +from .utils import * + + +headers = { + 'user-agent':( + "Mozilla/5.0 (Windows NT 10.0; WOW64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/62.0.3202.9 Safari/537.36" + ) +} + + +def date_parser_helper(days:int, parse:str="%d.%m.20%y"): + return dt.strftime( + dt.now() + + datetime.timedelta(days=days), + parse + ) + + +def docs_parse(): + + output = { + "data":{}, + "another_teacher":None + } + + page = requests.get(config.bot("link"), headers=headers) + page.encoding = 'utf-8' + + soup = BeautifulSoup(page.text, "html.parser") + + # Это в идеале нужно переписать... + try: output = table_parser(soup, output); #print(output) + except Exception: pass + try: output = one_parser(soup, output); #print(output) + except Exception: pass + try: output = parser_two(soup, output); print(output) + except Exception as e: raise(e) + #try: output = parser3(soup, output); print(output) + #except Exception as e: raise(e) + + + with open(config.data_file, 'w') as f: + json.dump(output, f, ensure_ascii=False) + f.close() + + +def get_about_replacements() -> dict: + with open(config.data_file, 'r') as f: + data = json.loads(f.read()) + f.close() + return data diff --git a/parser/utils.py b/parser/utils.py new file mode 100644 index 0000000..e81ddb4 --- /dev/null +++ b/parser/utils.py @@ -0,0 +1,68 @@ + +def table_parser(soup, output): + #Date parser + date = (soup.find("main").findAll('span', style="color:black"))[1] + output["date"] = date.text.replace(u'\xa0', u'') + + + #Replaces parser + replaces = soup.findAll('tr') + for data in replaces: + + text = ( + data.find("td", valign="top") + .find("span", style="color:black") + .text.replace(u'\xa0', u'') + ) + group = ( + data.find("span", style="color:black") + .text.replace(" ", "").replace(u'\xa0', u'')) + output["data"][group] = text + + return output + +def one_parser(soup, output): + raw_data = soup.find("main").findAll("p") + date = ( + raw_data[3].find("span", style="font-size:16px;").b.text.lower() + .replace(u"\xa0", u"").replace("на", "").replace("\r", "") + .replace("ЗАМІНИ ДО РОЗКЛАДУ".lower(), "").split("\n") + ) + output["date"] = date[1].lstrip(" ") + + for p in raw_data[4].find("span",style="font-size:16px;").b.text.replace(u"\xa0", u"").split("\n"): + p = p.lstrip(" ") + data_rep = (p.lstrip(" ").split(" ", 1)) + group = data_rep[0] + text = data_rep[1].replace("\r", "").lstrip(" ") + output["data"][group] = text + return output + +def parser_two(soup, output): + raw_data = soup.find("main").findAll("p")[2] + data = raw_data.text.split("\n") + output["date"] = data[1].replace("\r", "") + + for p in data[3:]: + r_data = p.split(maxsplit=1) + try: + group = r_data[0].replace(u"\xa0", u"").replace("\r", "") + text = r_data[1].replace(u"\xa0", u"").replace("\r", "") + except IndexError: break + output["data"][group] = text + return output + +def parser3(soup, output): + raw_data = soup.find("main").findAll("p") + + output["date"] = ( + raw_data[2].text + .replace("\r", "") + .replace("ЗАМІНИ НА", "").lstrip(" ").rstrip(" ").lower() + ) + for p in raw_data[5:]: + r_data = p.text.split("-", maxsplit=1) + group = r_data[0] + text = r_data[1] + output["data"][group] = text + return output diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6ab5307 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +#google-api-python-client +#google-auth-httplib2 +#google-auth-oauthlib +bs4 +peewee +aiogram +cryptography +pymysqldb +psycopg2 +aioschedule diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..a558a20 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1 @@ +from .bot_commands import set_commands \ No newline at end of file diff --git a/utils/announcements.py b/utils/announcements.py new file mode 100644 index 0000000..75d9a02 --- /dev/null +++ b/utils/announcements.py @@ -0,0 +1,27 @@ +import datetime +import asyncio +import aioschedule as schedule + +from load import dp, config +from parser import docs_parse + + +async def announce(): + date_now = datetime.datetime.today().weekday() + if (date_now == 5) or (date_now == 6): return + message = "Замены были обновлены, возможно появились изменения!)" + try: + docs_parse() + except Exception: + message = "Ошибка обновления данных!" + if config.admin_user is not None: + for user_id in config.admin_user: + await dp.bot.send_message(user_id, message) + + +async def scheduler(): + schedule.every(int(config.anons('time'))).seconds.do(announce) + + while True: + await schedule.run_pending() + await asyncio.sleep(5) diff --git a/utils/bot_commands.py b/utils/bot_commands.py new file mode 100644 index 0000000..ded322b --- /dev/null +++ b/utils/bot_commands.py @@ -0,0 +1,10 @@ +from aiogram import types + + +async def set_commands(dp): + await dp.bot.set_my_commands([ + types.BotCommand("start", "получить список замен"), + types.BotCommand("help", "информация"), + types.BotCommand("link", "получить ссылку на файл"), + types.BotCommand("reload", "только для администрации"), + ]) \ No newline at end of file