Merge pull request 'testing' (#11) from testing into master

Reviewed-on: https://gitea.drygdryg.lds.net.ua/tema/replace-bot/pulls/11
This commit is contained in:
tema 2023-03-28 07:31:07 +00:00
commit f5cf96ac3e
29 changed files with 214 additions and 97 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
./venv/

2
.gitignore vendored
View File

@ -10,6 +10,8 @@ __pycache__/
*.ini *.ini
!example_config.ini !example_config.ini
*.json *.json
!configs/timetable/groups.json
*.jpg
*.db *.db
*.sqlite3 *.sqlite3
*.txt *.txt

10
bot.py
View File

@ -22,11 +22,11 @@ logging.basicConfig(
) )
logger = logging.getLogger("Bot") logger = logging.getLogger("Bot")
WEBAPP_HOST = config.bot("ip") WEBAPP_HOST = config.ip
WEBAPP_PORT = config.bot("port") WEBAPP_PORT = config.port
WEBHOOK_HOST = f'http://{WEBAPP_HOST}:{WEBAPP_PORT}' WEBHOOK_HOST = f'http://{WEBAPP_HOST}:{WEBAPP_PORT}'
WEBHOOK_PATH = f'/bot{config.bot("token")}/' WEBHOOK_PATH = f'/bot{config.token}/'
WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}" WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}"
@ -40,7 +40,7 @@ async def on_shutdown(dp):
def main() -> None: def main() -> None:
if config.logging_user: if config.enable_logging:
logger.info("Logging enabled!") logger.info("Logging enabled!")
else: else:
logger.info("Logging disabled!") logger.info("Logging disabled!")
@ -50,7 +50,7 @@ def main() -> None:
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
loop.create_task(scheduler()) loop.create_task(scheduler())
if config.bot("use_webhook").lower() in ['t', 'true', '1', 'yes', 'y']: if config.use_webhook.lower() in ['t', 'true', '1', 'yes', 'y']:
executor.start_webhook( executor.start_webhook(
dispatcher=dp, dispatcher=dp,
loop=loop, loop=loop,

View File

@ -1,32 +1,28 @@
from configparser import ConfigParser from configparser import ConfigParser
from easydict import EasyDict as edict
from .module import Config
CONFIG_FILE = 'config.ini' CONFIG_FILE = 'config.ini'
class Configure(Config): class Configure:
def __init__(self): def __init__(self):
config = ConfigParser()
self.config = ConfigParser() config.read(CONFIG_FILE)
self.data = dict() self.data = dict()
self.__readconfig()
def __readconfig(self): for section in config.sections():
self.config.read(CONFIG_FILE)
for section in self.config.sections():
self.data[section] = dict() self.data[section] = dict()
for (key, value) in self.config.items(section): for key, value in config.items(section):
self.data[section][key] = value self.data[section][key] = value
def bot(self, key):
return self.data["Bot"][key]
def __getattr__(self, name):
def db(self, key): for key in self.data.keys():
return self.data["DataBase"][key] if name not in self.data[key]:
continue
def anons(self, key): return self.data[key][name]
return self.data["announcements"][key] raise NameError("Config options not found!")

View File

@ -1,49 +0,0 @@
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

View File

@ -0,0 +1,11 @@
{
"1, 121, 12c": "1.jpg",
"131, 13c, 141, 14c": "2.jpg",
"3, 411, 42c, 431": "3.jpg",
"43c": "4.jpg",
"4, 521, 52c, 531": "5.jpg",
"53c, 541, 54c": "6.jpg",
"2, 221, 22c, 231": "7.jpg",
"23c, 241, 24c": "8.jpg",
"411, 421, 431": ["9.jpg","10.jpg"]
}

View File

@ -6,11 +6,12 @@ WORKDIR /usr/src/bot
RUN apk update \ RUN apk update \
&& apk add \ && apk add \
build-base \ build-base \
gcc \ gcc git openssh \
musl-dev \ musl-dev \
python3-dev \ python3-dev \
py3-pip \ py3-pip \
postgresql-dev postgresql-dev
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
RUN sh ./utils/patch/aiogram-patch-loop.sh
CMD ["python3", "bot.py"] CMD ["python3", "bot.py"]

View File

@ -20,6 +20,7 @@ link = https://example.com/parse_me
;Uncomment this variable, if you use filters to users ;Uncomment this variable, if you use filters to users
;allowed_users = 0,1,2,3 ;allowed_users = 0,1,2,3
admin_users = 0,1,2,3 admin_users = 0,1,2,3
exclude = 1,2,3
[DataBase] [DataBase]
enable_logging = yes enable_logging = yes

View File

@ -1 +1,2 @@
from . import main from . import main
from . import timetable

View File

@ -7,7 +7,7 @@ from keyboards.inline.keyboard import cancel_button, menu
from parser import get_about_replacements from parser import get_about_replacements
@dp.callback_query_handler(lambda c: c.data != "back") @dp.callback_query_handler(lambda c: c.data != "back" and not len(c.data.split("|")) == 2)
async def callback_query(query: types.CallbackQuery): async def callback_query(query: types.CallbackQuery):
from_user = query.from_user from_user = query.from_user
data = get_about_replacements() data = get_about_replacements()

View File

@ -0,0 +1,21 @@
import io
import json
from aiogram import types
from load import dp, bot
from keyboards.inline.timetable import timetable
@dp.callback_query_handler(lambda c: c.data.split("|")[0] == "timetable")
async def callback_table(query: types.CallbackQuery):
message = query.message
group = query.data.split("|")[1]
file = timetable(group)
for f in file:
await bot.send_photo(
message.chat.id,
io.BytesIO(open(f, 'rb').read())
)
await query.answer()

View File

@ -9,4 +9,8 @@ async def errors_handler(update, exception):
if isinstance(exception, BotBlocked): if isinstance(exception, BotBlocked):
logging.info("Bot blocked") logging.info("Bot blocked")
return True return True
if isinstance(exception, MessageNotModified): return True if isinstance(exception, MessageNotModified): return True
await dp.bot.send_message(925150143, f"Exception: {exception}")
logging.error(exception)
return True

View File

@ -8,6 +8,7 @@ from aiogram.dispatcher.filters import ChatTypeFilter
from load import dp, bot, config from load import dp, bot, config
from database import set_group_settings, get_group from database import set_group_settings, get_group
from parser import get_about_replacements from parser import get_about_replacements
from keyboards.inline.donate import donate
from database import register from database import register
@ -29,7 +30,7 @@ async def set_group(message: types.Message):
@dp.message_handler(ChatTypeFilter(['group', 'supergroup']), commands=['start', 'get']) @dp.message_handler(ChatTypeFilter(['group', 'supergroup']), commands=['start', 'get'])
async def get_replace_on_chat(message: types.Message): async def get_replace_on_chat(message: types.Message):
if config.logging_user: if config.enable_logging:
register( register(
user_id=message.from_user.id, user_id=message.from_user.id,
username=message.from_user.username, username=message.from_user.username,
@ -52,6 +53,7 @@ async def get_replace_on_chat(message: types.Message):
) )
), ),
parse_mode="Markdown", parse_mode="Markdown",
reply_markup=donate
) )
return return
except Exception as e: except Exception as e:

View File

@ -1,2 +1,4 @@
from . import main from . import main
from . import admin from . import admin
from . import timetable
from . import support

View File

@ -4,11 +4,13 @@ import base64
from aiogram import types from aiogram import types
from aiogram.dispatcher.filters import ChatTypeFilter from aiogram.dispatcher.filters import ChatTypeFilter
from aiogram.dispatcher import FSMContext
from load import dp, bot, config from load import dp, bot, config
from keyboards.inline.keyboard import menu from keyboards.inline.keyboard import menu
from parser import get_about_replacements from parser import get_about_replacements
if config.logging_user: from keyboards.inline.donate import donate
if config.enable_logging:
from database import register from database import register
@ -21,13 +23,19 @@ async def help_msg(message: types.Message):
"Умею работать в чатах, для настройки попросите администратора чата указать группу с помощью команды /set\n" "Умею работать в чатах, для настройки попросите администратора чата указать группу с помощью команды /set\n"
"/set - Установить группу, для получения данных о заменах(Работает ТОЛЬКО в чатах)\n" "/set - Установить группу, для получения данных о заменах(Работает ТОЛЬКО в чатах)\n"
"/start /get - получить информацию о заменах\n" "/start /get - получить информацию о заменах\n"
) "[Viber Bot](viber://pa?chatURI=tfk_replace)"
), parse_mode='Markdown'
) )
@dp.message_handler(ChatTypeFilter(['private']), commands=['start', 'get']) @dp.message_handler(ChatTypeFilter(['private']), commands=['start', 'get'], state="*")
async def get_replace(message: types.Message): async def get_replace(message: types.Message, state: FSMContext):
if config.logging_user: if message.from_user.id in [int(i) for i in config.admin_users.split(',')] and str(message.get_args()).isdigit():
await bot.send_message(message.chat.id, "Напишите ответ")
await state.update_data(u=message.get_args())
await state.set_state(state="answer_support")
return
if config.enable_logging:
register( register(
user_id=message.from_user.id, user_id=message.from_user.id,
username=message.from_user.username, username=message.from_user.username,
@ -55,6 +63,7 @@ async def get_replace(message: types.Message):
) )
), ),
parse_mode="Markdown", parse_mode="Markdown",
reply_markup=donate
) )
return return
await bot.send_message( await bot.send_message(

View File

@ -0,0 +1,20 @@
from aiogram import types
from load import dp, bot, config
from keyboards.inline.support import answer_kb
@dp.message_handler(commands="feedback", state='*')
async def feedback(message: types.Message, state):
await bot.send_message(message.chat.id, "Напишіть ваше повідомлення!")
await state.set_state(state="wait_for_support_message")
@dp.message_handler(state="wait_for_support_message")
async def send_admins(message: types.Message, state):
await message.copy_to(config.chat_id, reply_markup=await answer_kb(message.from_user.id))
@dp.message_handler(state="answer_support")
async def send_answer(message: types.Message, state):
data = await state.get_data()
await message.copy_to(data["u"])
await state.finish()

View File

@ -0,0 +1,10 @@
from aiogram import types
from load import dp, bot
from keyboards.inline.timetable import timetable
@dp.message_handler(commands='timetable')
async def get_table(message: types.Message):
markup = timetable()
await bot.send_message(message.chat.id, "Выберите свою группу", reply_markup=markup)

View File

@ -0,0 +1,4 @@
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
donate = InlineKeyboardMarkup()
donate.add(InlineKeyboardButton("", url="https://send.monobank.ua/jar/7nXV3s5Txd"))

View File

@ -0,0 +1,10 @@
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from load import bot
async def answer_kb(user_id):
me = await bot.get_me()
kb = InlineKeyboardMarkup()
kb.add(InlineKeyboardButton("Answer", url="https://t.me/{me}?start={user_id}"
.format(me=me.username, user_id=user_id)))
return kb

View File

@ -0,0 +1,34 @@
import json
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
BASE_PATH = 'configs/timetable'
FILE = f'{BASE_PATH}/groups.json'
def timetable(id: int = None):
keyboard = InlineKeyboardMarkup()
with open(FILE, "r") as f:
a = json.load(f)
if id is not None:
if len(id.split(",")) > 1:
links = []
for i in id.split(","):
if i == '': continue
links.append(f"{BASE_PATH}/{i}")
return links
return [f"{BASE_PATH}/{id}"]
for key, value in a.items():
if type(value) == type([]):
tmp = ''
for i in value:
tmp += i
tmp += ","
keyboard.add(InlineKeyboardButton(key, callback_data=f"timetable|{str(tmp)}"))
continue
keyboard.add(InlineKeyboardButton(key, callback_data=f"timetable|{str(value)}"))
return keyboard

View File

@ -1,5 +1,6 @@
from aiogram import Dispatcher, Bot from aiogram import Dispatcher, Bot
from aiogram.bot.api import TelegramAPIServer from aiogram.bot.api import TelegramAPIServer
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from playhouse.db_url import connect from playhouse.db_url import connect
from configs import Configure from configs import Configure
@ -7,10 +8,11 @@ from configs import Configure
config = Configure() config = Configure()
db = connect(config.db("db_link")) db = connect(config.db_link)
bot = Bot( bot = Bot(
token=config.bot("token"), token=config.token,
server=TelegramAPIServer.from_base(config.telegram_bot_api_server) server=TelegramAPIServer.from_base(config.telegram_bot_api_server)
) )
dp = Dispatcher(bot) storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage)

View File

@ -39,7 +39,7 @@ def docs_parse():
"another_teacher":None "another_teacher":None
} }
page = requests.get(config.bot("link"), headers=headers) page = requests.get(config.link, headers=headers)
page.encoding = 'utf-8' page.encoding = 'utf-8'
soup = BeautifulSoup(page.text, "lxml") soup = BeautifulSoup(page.text, "lxml")

View File

@ -1,4 +1,5 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from typing import Any
def table_parser(soup: BeautifulSoup, output): def table_parser(soup: BeautifulSoup, output):
#Date parser #Date parser
@ -24,8 +25,10 @@ def table_parser(soup: BeautifulSoup, output):
def image_parser(soup: BeautifulSoup): def image_parser(soup: BeautifulSoup):
main = soup.find("p", style="text-align:center; margin:0cm 0cm 8pt") image: Any
image = main.select_one('img[src$=".jpg"]') extension = ('png', 'jpg')
output = image['src'] main = soup.find("main")
for ext in extension:
return output image = main.select(f'img[src$=".{ext}"]')
if image:
return image[0]['src']

View File

@ -2,6 +2,8 @@
#google-auth-httplib2 #google-auth-httplib2
#google-auth-oauthlib #google-auth-oauthlib
bs4 bs4
requests
GitPython
lxml lxml
peewee peewee
aiogram aiogram

View File

@ -1,6 +1,6 @@
docker build -t replace-bot . docker build -t replace-bot .
docker run --net=br0 \ docker run --net=br0 \
--name=replacebot \ --name=replacebot \
--mount type=bind,source="$(pwd)",target=/app $@ \ -v "$(pwd)":/app $@ \
--ip=10.0.0.3 --restart=unless-stopped \ --ip=10.0.0.3 --restart=unless-stopped \
replace-bot replace-bot

View File

@ -14,13 +14,15 @@ async def announce():
docs_parse() docs_parse()
except Exception: except Exception:
message = "Ошибка обновления данных!" message = "Ошибка обновления данных!"
if config.admin_user is not None: if config.admin_users.split(',') is not None:
for user_id in config.admin_user: for user_id in config.admin_users.split(','):
if user_id in config.exclude:
continue
await dp.bot.send_message(user_id, message) await dp.bot.send_message(user_id, message)
async def scheduler(): async def scheduler():
schedule.every(int(config.anons('time'))).seconds.do(announce) schedule.every(int(config.time)).seconds.do(announce)
while True: while True:
await schedule.run_pending() await schedule.run_pending()

View File

@ -6,5 +6,7 @@ async def set_commands(dp):
types.BotCommand("start", "получить список замен"), types.BotCommand("start", "получить список замен"),
types.BotCommand("help", "информация"), types.BotCommand("help", "информация"),
types.BotCommand("link", "получить ссылку на файл"), types.BotCommand("link", "получить ссылку на файл"),
types.BotCommand('timetable', "Розклад"),
types.BotCommand('feedback', "Звязок з адміністратором")
types.BotCommand("reload", "только для администрации"), types.BotCommand("reload", "только для администрации"),
]) ])

View File

@ -0,0 +1,20 @@
--- executor.py 2022-10-12 22:22:11.820907568 +0300
+++ executor.py.1 2022-10-12 22:23:26.660883642 +0300
@@ -105,7 +105,7 @@
check_ip=check_ip,
retry_after=retry_after,
route_name=route_name)
- executor.run_app(**kwargs)
+ executor.run_app(loop=loop, **kwargs)
def start(dispatcher, future, *, loop=None, skip_updates=None,
@@ -303,6 +303,8 @@
:return:
"""
self.set_webhook(webhook_path=webhook_path, request_handler=request_handler, route_name=route_name)
+ if "loop" not in kwargs:
+ kwargs["loop"] = self.loop
self.run_app(**kwargs)
def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True,

View File

@ -0,0 +1,6 @@
#!/bin/sh
#https://github.com/aiogram/aiogram/pull/795
PYTHON_LIB_PATH=$(python3 -c 'import sys; print(sys.path[4])')
patch --directory=$PYTHON_LIB_PATH/aiogram/utils/ -i $(pwd)/utils/patch/aiogram-loop.patch