Init commit
This commit is contained in:
commit
8060b933a5
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -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/
|
38
backup/google_parser/README.md
Normal file
38
backup/google_parser/README.md
Normal file
@ -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!**
|
31
backup/google_parser/example_config.ini
Normal file
31
backup/google_parser/example_config.ini
Normal file
@ -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
|
||||
;////////////
|
2
backup/google_parser/parser/__init__.py
Normal file
2
backup/google_parser/parser/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .parser import get_about_replacements, docs_parse
|
||||
__all__ = ['get_about_replacements', 'docs_parse']
|
108
backup/google_parser/parser/parser.py
Normal file
108
backup/google_parser/parser/parser.py
Normal file
@ -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,
|
||||
}
|
188
backup/google_parser/parser/utils.py
Normal file
188
backup/google_parser/parser/utils.py
Normal file
@ -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
|
9
backup/google_parser/requirements.txt
Normal file
9
backup/google_parser/requirements.txt
Normal file
@ -0,0 +1,9 @@
|
||||
google-api-python-client
|
||||
google-auth-httplib2
|
||||
google-auth-oauthlib
|
||||
peewee
|
||||
aiogram
|
||||
cryptography
|
||||
pymysqldb
|
||||
psycopg2
|
||||
aioschedule
|
9
backup/google_parser/setup_google_docs_api.py
Normal file
9
backup/google_parser/setup_google_docs_api.py
Normal file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
'''
|
||||
Don`t move this file!
|
||||
'''
|
||||
|
||||
if __name__ == '__main__':
|
||||
from parser import docs_parse
|
||||
docs_parse()
|
69
bot.py
Executable file
69
bot.py
Executable file
@ -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()
|
1
configs/__init__.py
Normal file
1
configs/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .configure import Configure
|
32
configs/configure.py
Normal file
32
configs/configure.py
Normal file
@ -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]
|
49
configs/module.py
Normal file
49
configs/module.py
Normal file
@ -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
|
2
database/__init__.py
Normal file
2
database/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
__all__ = ["register", "get_all_users"]
|
||||
from .worker import register, get_all_users, set_group_settings, get_group
|
26
database/models.py
Normal file
26
database/models.py
Normal file
@ -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
|
||||
|
52
database/worker.py
Normal file
52
database/worker.py
Normal file
@ -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
|
||||
|
16
dockerfile
Normal file
16
dockerfile
Normal file
@ -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"]
|
91
engineering_works.py
Normal file
91
engineering_works.py
Normal file
@ -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)
|
31
example_config.ini
Normal file
31
example_config.ini
Normal file
@ -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
|
||||
;////////////
|
7
filters/__init__.py
Normal file
7
filters/__init__.py
Normal file
@ -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)
|
36
filters/main.py
Normal file
36
filters/main.py
Normal file
@ -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
|
||||
|
4
handlers/__init__.py
Normal file
4
handlers/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from . import groups
|
||||
from . import private
|
||||
from . import callback
|
||||
from . import errors
|
1
handlers/callback/__init__.py
Normal file
1
handlers/callback/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import main
|
56
handlers/callback/main.py
Normal file
56
handlers/callback/main.py
Normal file
@ -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()
|
1
handlers/errors/__init__.py
Normal file
1
handlers/errors/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import main
|
11
handlers/errors/main.py
Normal file
11
handlers/errors/main.py
Normal file
@ -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
|
1
handlers/groups/__init__.py
Normal file
1
handlers/groups/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import main
|
64
handlers/groups/main.py
Normal file
64
handlers/groups/main.py
Normal file
@ -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("Похоже администратор группы не настроил привязку")
|
2
handlers/private/__init__.py
Normal file
2
handlers/private/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import main
|
||||
from . import admin
|
23
handlers/private/admin.py
Normal file
23
handlers/private/admin.py
Normal file
@ -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(
|
||||
"Произойшла ошибка!"
|
||||
)
|
82
handlers/private/main.py
Normal file
82
handlers/private/main.py
Normal file
@ -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 = (
|
||||
'<a href="{}">Проверьте замены тут</a>'
|
||||
.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 = (
|
||||
'<a href="{}">Проверьте замены тут</a>'
|
||||
.format(config.bot("link"))
|
||||
)
|
||||
await bot.send_message(
|
||||
message.chat.id,
|
||||
msg,
|
||||
parse_mode='HTML',
|
||||
disable_web_page_preview=True
|
||||
)
|
2
keyboards/__init__.py
Normal file
2
keyboards/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import inline
|
||||
from . import default
|
0
keyboards/default/__init__.py
Normal file
0
keyboards/default/__init__.py
Normal file
1
keyboards/inline/__init__.py
Normal file
1
keyboards/inline/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import keyboard
|
21
keyboards/inline/keyboard.py
Normal file
21
keyboards/inline/keyboard.py
Normal file
@ -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")
|
||||
]
|
||||
]
|
||||
)
|
16
load.py
Normal file
16
load.py
Normal file
@ -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)
|
2
parser/__init__.py
Normal file
2
parser/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .parser import get_about_replacements, docs_parse
|
||||
__all__ = ['get_about_replacements', 'docs_parse']
|
63
parser/parser.py
Normal file
63
parser/parser.py
Normal file
@ -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
|
68
parser/utils.py
Normal file
68
parser/utils.py
Normal file
@ -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
|
10
requirements.txt
Normal file
10
requirements.txt
Normal file
@ -0,0 +1,10 @@
|
||||
#google-api-python-client
|
||||
#google-auth-httplib2
|
||||
#google-auth-oauthlib
|
||||
bs4
|
||||
peewee
|
||||
aiogram
|
||||
cryptography
|
||||
pymysqldb
|
||||
psycopg2
|
||||
aioschedule
|
1
utils/__init__.py
Normal file
1
utils/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .bot_commands import set_commands
|
27
utils/announcements.py
Normal file
27
utils/announcements.py
Normal file
@ -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)
|
10
utils/bot_commands.py
Normal file
10
utils/bot_commands.py
Normal file
@ -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", "только для администрации"),
|
||||
])
|
Loading…
Reference in New Issue
Block a user