Archived
some upd
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
FROM python:3.9-slim
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y cron
|
||||||
|
# RUN apt-get install -y cron
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt requirements.txt
|
COPY requirements.txt requirements.txt
|
||||||
@@ -7,4 +10,6 @@ RUN pip install -r requirements.txt
|
|||||||
|
|
||||||
COPY ./app .
|
COPY ./app .
|
||||||
|
|
||||||
|
RUN chmod +x install.sh
|
||||||
|
|
||||||
CMD ["./install.sh"]
|
CMD ["./install.sh"]
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
#!/usr/bin/python3.3
|
#!/usr/bin/python3.3
|
||||||
import json, yaml, schedule
|
import schedule
|
||||||
import schedules as bot
|
import schedules as bot
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from sshtunnel import SSHTunnelForwarder
|
from sshtunnel import SSHTunnelForwarder
|
||||||
from psycopg2 import pool
|
from psycopg2 import pool
|
||||||
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
|
||||||
allow_update = True
|
allow_update = True
|
||||||
@@ -107,11 +108,12 @@ def parse_lesson(lesson: dict[str, str]) -> str:
|
|||||||
if lesson["lector"]:
|
if lesson["lector"]:
|
||||||
if "/" not in lesson["lector"]:
|
if "/" not in lesson["lector"]:
|
||||||
text = "{}-{} | {}\
|
text = "{}-{} | {}\
|
||||||
\n<b>{}</b>\
|
\n{}<b>{}</b>\
|
||||||
\n<i>{}</i>".format(
|
\n<i>{}</i>".format(
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
lesson["room"],
|
lesson["room"],
|
||||||
|
"🎓 " if lesson["is_lecture"] else "",
|
||||||
lesson["course"],
|
lesson["course"],
|
||||||
lesson["lector"],
|
lesson["lector"],
|
||||||
)
|
)
|
||||||
@@ -119,7 +121,8 @@ def parse_lesson(lesson: dict[str, str]) -> str:
|
|||||||
lectors = lesson["lector"].split("/")
|
lectors = lesson["lector"].split("/")
|
||||||
text = "{}-{} | {}\
|
text = "{}-{} | {}\
|
||||||
\n<b>{}</b>\
|
\n<b>{}</b>\
|
||||||
\n<i>{}</i> / <i>{}</i>".format(
|
\n<i>{}</i>\
|
||||||
|
\n<i>{}</i>".format(
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
lesson["room"].replace("/", ", "),
|
lesson["room"].replace("/", ", "),
|
||||||
@@ -129,12 +132,15 @@ def parse_lesson(lesson: dict[str, str]) -> str:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
text = "{}-{}\
|
text = "{}-{}\
|
||||||
\n<b>{}</b>".format(
|
\n{}<b>{}</b>".format(
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
"🎓 " if lesson["is_lecture"] else "",
|
||||||
lesson["course"],
|
lesson["course"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# text += f"\n{lesson['info']}"
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
@@ -148,6 +154,12 @@ def create_schedule_tasks(manual=False):
|
|||||||
allow_update = True
|
allow_update = True
|
||||||
|
|
||||||
schedule.clear()
|
schedule.clear()
|
||||||
|
|
||||||
|
def run_script():
|
||||||
|
os.system("python3 -u upload_sql.py")
|
||||||
|
|
||||||
|
schedule.every(6).minutes.do(run_script)
|
||||||
|
|
||||||
conn = db.getconn()
|
conn = db.getconn()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT * FROM users")
|
cursor.execute("SELECT * FROM users")
|
||||||
@@ -155,7 +167,7 @@ def create_schedule_tasks(manual=False):
|
|||||||
users = into_list(data, cursor.description)
|
users = into_list(data, cursor.description)
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
if user["allow_message"] != "yes":
|
if not user["allow_message"]:
|
||||||
continue
|
continue
|
||||||
group = user["group"]
|
group = user["group"]
|
||||||
if group == "other":
|
if group == "other":
|
||||||
@@ -179,7 +191,7 @@ def create_schedule_tasks(manual=False):
|
|||||||
|
|
||||||
thread_id = user["thread"]
|
thread_id = user["thread"]
|
||||||
|
|
||||||
delta = "00:" + user["timeout"]
|
delta = "00:" + str(user["timeout"])
|
||||||
format = "%H:%M"
|
format = "%H:%M"
|
||||||
start_task = datetime.strptime(start, format) - datetime.strptime(
|
start_task = datetime.strptime(start, format) - datetime.strptime(
|
||||||
delta, format
|
delta, format
|
||||||
@@ -243,7 +255,7 @@ def students_in_group(group) -> list[str]:
|
|||||||
return students
|
return students
|
||||||
|
|
||||||
|
|
||||||
def return_infos(id) -> dict[str, str] | None:
|
def return_infos(id) -> Optional[Dict[str, str]]:
|
||||||
conn = db.getconn()
|
conn = db.getconn()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT * FROM users WHERE id = %s", (id,))
|
cursor.execute("SELECT * FROM users WHERE id = %s", (id,))
|
||||||
@@ -259,6 +271,12 @@ def pause_bot():
|
|||||||
|
|
||||||
if allow_update:
|
if allow_update:
|
||||||
schedule.clear()
|
schedule.clear()
|
||||||
|
|
||||||
|
def run_script():
|
||||||
|
os.system("python3 -u upload_sql.py")
|
||||||
|
|
||||||
|
schedule.every(6).minutes.do(run_script)
|
||||||
|
|
||||||
allow_update = False
|
allow_update = False
|
||||||
bot.send_admin_message("Бот больше не отправляет расписание")
|
bot.send_admin_message("Бот больше не отправляет расписание")
|
||||||
else:
|
else:
|
||||||
|
|||||||
+14
-11
@@ -1,19 +1,22 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# List of environment variables to check
|
# List of environment variables to check
|
||||||
env_vars=("TG_ADMIN_ID" "TG_API_TOKEN" "DB_HOST" "DB_PORT" "DB_USER" "DB_PASSWORD" "DB_NAME" "TABLE_ID")
|
# env_vars=("TG_ADMIN_ID" "TG_API_TOKEN" "DB_HOST" "DB_PORT" "DB_USER" "DB_PASSWORD" "DB_NAME" "TABLE_ID")
|
||||||
|
|
||||||
for var in "${env_vars[@]}"; do
|
# for var in "${env_vars[@]}"; do
|
||||||
if [ -z "${!var}" ]; then
|
# if [ -z "${!var}" ]; then
|
||||||
echo "Environment variable $var is not set or is empty."
|
# echo "Environment variable $var is not set or is empty."
|
||||||
exit 1
|
# exit 1
|
||||||
# else
|
# # else
|
||||||
# echo "Environment variable $var is set to '${!var}'."
|
# # echo "Environment variable $var is set to '${!var}'."
|
||||||
fi
|
# fi
|
||||||
done
|
# done
|
||||||
|
|
||||||
# Add cron jobs to crontab
|
# Add cron jobs to crontab
|
||||||
(crontab -l 2>/dev/null; echo "0 * * * * python3 /app/upload_sql.py") | crontab -
|
# (crontab -l 2>/dev/null; echo "0 * * * * python3 /app/upload_sql.py") | crontab -
|
||||||
|
# (crontab -l 2>/dev/null; echo "*/14 * * * * python3 /app/upload_sql.py") | crontab -
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Start the schedules.py script
|
# Start the schedules.py script
|
||||||
python3 -u /app/schedules.py &
|
python3 -u /app/schedules.py
|
||||||
|
|||||||
+17
-5
@@ -10,7 +10,14 @@ from dotenv import load_dotenv
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
bot = telebot.TeleBot(os.environ.get("TG_TEST_TOKEN"), parse_mode=ParseMode.HTML)
|
bot = telebot.TeleBot(
|
||||||
|
(
|
||||||
|
os.environ.get("TG_TEST_TOKEN")
|
||||||
|
if os.environ.get("ENV") == "dev"
|
||||||
|
else os.environ.get("TG_API_TOKEN")
|
||||||
|
),
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
)
|
||||||
|
|
||||||
PARITY_FIRST = 0
|
PARITY_FIRST = 0
|
||||||
|
|
||||||
@@ -476,15 +483,15 @@ def send_info(message):
|
|||||||
def pause_schedule(message):
|
def pause_schedule(message):
|
||||||
infos = db.return_infos(message.chat.id)["allow_message"]
|
infos = db.return_infos(message.chat.id)["allow_message"]
|
||||||
|
|
||||||
if infos == "yes":
|
if infos == True:
|
||||||
db.change_user_param(str(message.chat.id), "allow_message", "no")
|
db.change_user_param(str(message.chat.id), "allow_message", False)
|
||||||
send_message(
|
send_message(
|
||||||
message.chat.id,
|
message.chat.id,
|
||||||
"Рассылка сообщений прекращена. \
|
"Рассылка сообщений прекращена. \
|
||||||
\nДля возобновления воспользуйтесь командой\n/pause",
|
\nДля возобновления воспользуйтесь командой\n/pause",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
db.change_user_param(str(message.chat.id), "allow_message", "yes")
|
db.change_user_param(str(message.chat.id), "allow_message", True)
|
||||||
send_message(message.chat.id, "Рассылка сообщений возоблена!")
|
send_message(message.chat.id, "Рассылка сообщений возоблена!")
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -614,7 +621,7 @@ def send_message(id, text, thread_id="General", parity=None):
|
|||||||
thread_id = None
|
thread_id = None
|
||||||
|
|
||||||
if parity and parity != "-":
|
if parity and parity != "-":
|
||||||
parity = int(parity)
|
parity = 0 if parity == "чёт" else 1 if parity == "нечёт" else None
|
||||||
count_of_weeks = (datetime.now() - datetime(2024, 2, 5)).days // 7
|
count_of_weeks = (datetime.now() - datetime(2024, 2, 5)).days // 7
|
||||||
is_odd = (count_of_weeks + 1) % 2 == PARITY_FIRST
|
is_odd = (count_of_weeks + 1) % 2 == PARITY_FIRST
|
||||||
|
|
||||||
@@ -674,6 +681,11 @@ if __name__ == "__main__":
|
|||||||
target=bot.infinity_polling, name="bot_infinity_polling", daemon=True
|
target=bot.infinity_polling, name="bot_infinity_polling", daemon=True
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
|
def run_script():
|
||||||
|
os.system("python3 -u upload_sql.py")
|
||||||
|
|
||||||
|
schedule.every(6).minutes.do(run_script)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
schedule.run_pending()
|
schedule.run_pending()
|
||||||
|
|||||||
+69
-109
@@ -5,69 +5,13 @@ from dotenv import load_dotenv
|
|||||||
from google.oauth2 import service_account
|
from google.oauth2 import service_account
|
||||||
from sshtunnel import SSHTunnelForwarder
|
from sshtunnel import SSHTunnelForwarder
|
||||||
from psycopg2 import pool
|
from psycopg2 import pool
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
bot = telebot.TeleBot(os.environ.get("TG_TEST_TOKEN"))
|
||||||
|
|
||||||
"""
|
SERVICE_ACCOUNT_FILE = "credentials.json"
|
||||||
CREATE TABLE IF NOT EXISTS schedule(
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
day_of_week VARCHAR(255),
|
|
||||||
begin_time VARCHAR(20),
|
|
||||||
end_time VARCHAR(20),
|
|
||||||
course VARCHAR(255),
|
|
||||||
lector VARCHAR(255),
|
|
||||||
is_lecture BOOLEAN DEFAULT FALSE,
|
|
||||||
room VARCHAR(20),
|
|
||||||
group VARCHAR(10),
|
|
||||||
parity VARCHAR(10),
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
|
|
||||||
"""
|
|
||||||
День недели Время начала Название Чётность пары Лекция? Преподаватель Кабинет 2-ой преподаватель Прочие кабинеты Остальная информация
|
|
||||||
Понедельник 8:45 Английский язык - FALSE Бим Мария Моисеевна 735 Шубина Юлия Витальевна 790
|
|
||||||
Понедельник 10:30 Практикум на ЭВМ - FALSE Тюляева Вера Викторовна МЗ-4
|
|
||||||
Понедельник 12:15 Действительный и комплексный анализ - TRUE Крицков Леонид Владимирович П-12
|
|
||||||
Понедельник 15:00 Физическая культура - FALSE
|
|
||||||
Понедельник 16:20 FALSE
|
|
||||||
Вторник 8:45 Теория вероятностей и математическая статистика - TRUE Королёв Виктор Юрьевич П-13
|
|
||||||
Вторник 10:30 Обыкновенные дифференциальные уравнения - FALSE Дмитриева Ирина Владимировна 607
|
|
||||||
Вторник 12:15 Действительный и комплексный анализ - FALSE Сычугов Дмитрий Юрьевич 613
|
|
||||||
Вторник 14:35 Электродинамика - FALSE Аксенов Владимир Николаевич 659
|
|
||||||
Вторник 16:20 - FALSE
|
|
||||||
Среда 8:45 Практикум на ЭВМ - FALSE Тюляева Вера Викторовна 505
|
|
||||||
Среда 10:30 Теория вероятностей и математическая статистика - FALSE Ирина Геннадьевна Шевцова 645
|
|
||||||
Среда 12:15 Обыкновенные дифференциальные уравнения - TRUE Денисов Александр Михайлович П-13
|
|
||||||
Среда 15:00 FALSE
|
|
||||||
Среда 16:20 FALSE
|
|
||||||
Четверг 9:00 FALSE
|
|
||||||
Четверг 10:30 FALSE
|
|
||||||
Четверг 12:15 FALSE
|
|
||||||
Четверг 15:00 FALSE
|
|
||||||
Четверг 16:20 FALSE
|
|
||||||
Пятница 9:00 FALSE
|
|
||||||
Пятница 10:30 FALSE
|
|
||||||
Пятница 12:15 FALSE
|
|
||||||
Пятница 15:00 FALSE
|
|
||||||
Пятница 16:20 FALSE
|
|
||||||
Суббота 9:00 FALSE
|
|
||||||
Суббота 10:30 FALSE
|
|
||||||
Суббота 12:15 FALSE
|
|
||||||
Суббота 15:00 FALSE
|
|
||||||
Суббота 16:20 FALSE
|
|
||||||
FALSE
|
|
||||||
FALSE
|
|
||||||
FALSE
|
|
||||||
FALSE
|
|
||||||
FALSE
|
|
||||||
FALSE
|
|
||||||
FALSE
|
|
||||||
FALSE
|
|
||||||
FALSE
|
|
||||||
"""
|
|
||||||
|
|
||||||
SERVICE_ACCOUNT_FILE = "app/credentials.json"
|
|
||||||
|
|
||||||
credentials = service_account.Credentials.from_service_account_file(
|
credentials = service_account.Credentials.from_service_account_file(
|
||||||
SERVICE_ACCOUNT_FILE,
|
SERVICE_ACCOUNT_FILE,
|
||||||
@@ -81,10 +25,6 @@ gc = gspread.authorize(credentials)
|
|||||||
|
|
||||||
table = gc.open_by_key(os.environ.get("TABLE_ID"))
|
table = gc.open_by_key(os.environ.get("TABLE_ID"))
|
||||||
|
|
||||||
ws = table.worksheets()
|
|
||||||
|
|
||||||
bot = telebot.TeleBot(os.environ.get("TG_TEST_TOKEN"))
|
|
||||||
|
|
||||||
if os.environ.get("ENV") == "dev":
|
if os.environ.get("ENV") == "dev":
|
||||||
print("DEV: Connecting to local database")
|
print("DEV: Connecting to local database")
|
||||||
server = SSHTunnelForwarder(
|
server = SSHTunnelForwarder(
|
||||||
@@ -113,52 +53,72 @@ db = pool.SimpleConnectionPool(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
for i in ws:
|
def f():
|
||||||
print(i.title)
|
ws = table.worksheets()
|
||||||
|
|
||||||
if not (i.title == "Пример" or i.title == "main") and i.title.isdigit():
|
for i in ws:
|
||||||
df = pd.DataFrame(i.get_all_records())
|
# print(i.title)
|
||||||
if not df.empty:
|
|
||||||
with db.getconn() as conn:
|
if not (i.title == "Пример" or i.title == "main") and i.title.isdigit():
|
||||||
with conn.cursor() as cursor:
|
df = pd.DataFrame(i.get_all_records())
|
||||||
cursor.execute(
|
if not df.empty:
|
||||||
'DELETE FROM schedule WHERE "group" = %s', (str(i.title),)
|
with db.getconn() as conn:
|
||||||
)
|
with conn.cursor() as cursor:
|
||||||
conn.commit()
|
|
||||||
for index, row in df.iterrows():
|
|
||||||
day_of_week_map = {
|
|
||||||
"Понедельник": "mon",
|
|
||||||
"Вторник": "tue",
|
|
||||||
"Среда": "wed",
|
|
||||||
"Четверг": "thu",
|
|
||||||
"Пятница": "fri",
|
|
||||||
"Суббота": "sat",
|
|
||||||
"Воскресенье": "sun",
|
|
||||||
}
|
|
||||||
row["День недели"] = day_of_week_map.get(
|
|
||||||
row["День недели"], row["День недели"]
|
|
||||||
)
|
|
||||||
if row["Название"] == "":
|
|
||||||
continue
|
|
||||||
end_time = (
|
|
||||||
pd.to_datetime(row["Время начала"], format="%H:%M")
|
|
||||||
+ pd.Timedelta(hours=1, minutes=35)
|
|
||||||
).strftime("%H:%M")
|
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
'DELETE FROM schedule WHERE "group" = %s', (str(i.title),)
|
||||||
INSERT INTO schedule (day_of_week, begin_time, end_time, course, lector, is_lecture, room, "group", parity)
|
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
row["День недели"],
|
|
||||||
row["Время начала"],
|
|
||||||
end_time,
|
|
||||||
row["Название"],
|
|
||||||
f'{row["Преподаватель"]}{"/" + str(row["2-ой преподаватель"]) if row["2-ой преподаватель"] else ""}',
|
|
||||||
row["Лекция?"],
|
|
||||||
f'{row["Кабинет"]}{"/" + str(row["Прочие кабинеты"]) if row["Прочие кабинеты"] else ""}',
|
|
||||||
i.title,
|
|
||||||
row["Чётность пары"],
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
day_of_week_map = {
|
||||||
|
"Понедельник": "mon",
|
||||||
|
"Вторник": "tue",
|
||||||
|
"Среда": "wed",
|
||||||
|
"Четверг": "thu",
|
||||||
|
"Пятница": "fri",
|
||||||
|
"Суббота": "sat",
|
||||||
|
"Воскресенье": "sun",
|
||||||
|
}
|
||||||
|
row["День недели"] = day_of_week_map.get(
|
||||||
|
row["День недели"], row["День недели"]
|
||||||
|
)
|
||||||
|
if row["Название"] == "":
|
||||||
|
continue
|
||||||
|
end_time = (
|
||||||
|
pd.to_datetime(row["Время начала"], format="%H:%M")
|
||||||
|
+ pd.Timedelta(hours=1, minutes=35)
|
||||||
|
).strftime("%H:%M")
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO schedule (day_of_week, begin_time, end_time, course, lector, is_lecture, room, "group", parity)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
row["День недели"],
|
||||||
|
row["Время начала"],
|
||||||
|
end_time,
|
||||||
|
row["Название"],
|
||||||
|
f'{row["Преподаватель"]}{"/" + str(row["2-ой преподаватель"]) if row["2-ой преподаватель"] else ""}',
|
||||||
|
row["Лекция?"],
|
||||||
|
f'{row["Кабинет"]}{"/" + str(row["Прочие кабинеты"]) if row["Прочие кабинеты"] else ""}',
|
||||||
|
i.title,
|
||||||
|
row["Чётность пары"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
text = f"Last tg-bot update was at: {datetime.now().astimezone().strftime('%Y-%m-%d %H:%M %Z')}"
|
||||||
|
table.get_worksheet(0).update_acell("A25", text)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
f()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
bot.send_message(int(os.environ.get("ADMIN_ID")), "Check logs! upload error.")
|
||||||
|
bot.send_message(int(os.environ.get("ADMIN_ID")), e)
|
||||||
|
|
||||||
|
exit(0)
|
||||||
|
|||||||
+8
-2
@@ -6,15 +6,21 @@ services:
|
|||||||
container_name: schedule_bot
|
container_name: schedule_bot
|
||||||
environment:
|
environment:
|
||||||
- ENV=production
|
- ENV=production
|
||||||
|
- TZ=Europe/Moscow
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- postgres_postgres
|
- postgres_postgres
|
||||||
|
volumes:
|
||||||
|
- "/etc/timezone:/etc/timezone:ro"
|
||||||
|
- "/etc/localtime:/etc/localtime:ro"
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpus: '0.3'
|
cpus: '0.3'
|
||||||
memory: 200M
|
memory: 300M # Set a hard memory limit
|
||||||
|
reservations:
|
||||||
|
memory: 200M # Reserve a soft memory limit
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
postgres_postgres:
|
postgres_postgres:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
Reference in New Issue
Block a user