From 8060b933a53ea1eb72457c76d754e5b4f569473c Mon Sep 17 00:00:00 2001
From: Jemacivan <63158240+Jemacivan@users.noreply.github.com>
Date: Wed, 16 Feb 2022 17:13:44 +0200
Subject: [PATCH] Init commit
---
.gitignore | 18 ++
backup/google_parser/README.md | 38 ++++
backup/google_parser/example_config.ini | 31 +++
backup/google_parser/parser/__init__.py | 2 +
backup/google_parser/parser/parser.py | 108 ++++++++++
backup/google_parser/parser/utils.py | 188 ++++++++++++++++++
backup/google_parser/requirements.txt | 9 +
backup/google_parser/setup_google_docs_api.py | 9 +
bot.py | 69 +++++++
configs/__init__.py | 1 +
configs/configure.py | 32 +++
configs/module.py | 49 +++++
database/__init__.py | 2 +
database/models.py | 26 +++
database/worker.py | 52 +++++
dockerfile | 16 ++
engineering_works.py | 91 +++++++++
example_config.ini | 31 +++
filters/__init__.py | 7 +
filters/main.py | 36 ++++
handlers/__init__.py | 4 +
handlers/callback/__init__.py | 1 +
handlers/callback/main.py | 56 ++++++
handlers/errors/__init__.py | 1 +
handlers/errors/main.py | 11 +
handlers/groups/__init__.py | 1 +
handlers/groups/main.py | 64 ++++++
handlers/private/__init__.py | 2 +
handlers/private/admin.py | 23 +++
handlers/private/main.py | 82 ++++++++
keyboards/__init__.py | 2 +
keyboards/default/__init__.py | 0
keyboards/inline/__init__.py | 1 +
keyboards/inline/keyboard.py | 21 ++
load.py | 16 ++
parser/__init__.py | 2 +
parser/parser.py | 63 ++++++
parser/utils.py | 68 +++++++
requirements.txt | 10 +
utils/__init__.py | 1 +
utils/announcements.py | 27 +++
utils/bot_commands.py | 10 +
42 files changed, 1281 insertions(+)
create mode 100644 .gitignore
create mode 100644 backup/google_parser/README.md
create mode 100644 backup/google_parser/example_config.ini
create mode 100644 backup/google_parser/parser/__init__.py
create mode 100644 backup/google_parser/parser/parser.py
create mode 100644 backup/google_parser/parser/utils.py
create mode 100644 backup/google_parser/requirements.txt
create mode 100644 backup/google_parser/setup_google_docs_api.py
create mode 100755 bot.py
create mode 100644 configs/__init__.py
create mode 100644 configs/configure.py
create mode 100644 configs/module.py
create mode 100644 database/__init__.py
create mode 100644 database/models.py
create mode 100644 database/worker.py
create mode 100644 dockerfile
create mode 100644 engineering_works.py
create mode 100644 example_config.ini
create mode 100644 filters/__init__.py
create mode 100644 filters/main.py
create mode 100644 handlers/__init__.py
create mode 100644 handlers/callback/__init__.py
create mode 100644 handlers/callback/main.py
create mode 100644 handlers/errors/__init__.py
create mode 100644 handlers/errors/main.py
create mode 100644 handlers/groups/__init__.py
create mode 100644 handlers/groups/main.py
create mode 100644 handlers/private/__init__.py
create mode 100644 handlers/private/admin.py
create mode 100644 handlers/private/main.py
create mode 100644 keyboards/__init__.py
create mode 100644 keyboards/default/__init__.py
create mode 100644 keyboards/inline/__init__.py
create mode 100644 keyboards/inline/keyboard.py
create mode 100644 load.py
create mode 100644 parser/__init__.py
create mode 100644 parser/parser.py
create mode 100644 parser/utils.py
create mode 100644 requirements.txt
create mode 100644 utils/__init__.py
create mode 100644 utils/announcements.py
create mode 100644 utils/bot_commands.py
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