Compare commits

..

No commits in common. "f330e3dff3134380d45a32f4974c15a430d5ff2a" and "c2077a85b3c5fa5c5891c04532ee3b007cb05316" have entirely different histories.

6 changed files with 125 additions and 168 deletions

View file

@ -1,4 +0,0 @@
TOKEN=
MAIN_GROUP_ID=
EDIT_MESSAGE_ID=
ADD_CALENDAR_LINK=

1
.gitignore vendored
View file

@ -1 +0,0 @@
.env

View file

@ -1,15 +0,0 @@
FROM python:3.12-bookworm
STOPSIGNAL SIGINT
WORKDIR /app
# make ru_RU.UTF-8 locale available
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install \
--no-install-recommends -y locales && rm -r /var/lib/apt/lists/* && \
printf "en_US.UTF-8 UTF-8\nru_RU.UTF-8 UTF-8" >/etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY main.py .
CMD ["python3", "-u", "main.py"]

View file

@ -1,7 +0,0 @@
services:
deadline_bot:
build: .
env_file: .env
volumes:
- /etc/localtime:/etc/localtime:ro
restart: unless-stopped

244
main.py
View file

@ -1,12 +1,12 @@
import datetime as dt
import locale
import datetime
import logging
import os
import re
import time
import urllib.parse
import requests
import asyncio
from aiogram import Bot
import datetime as dt
import locale
import urllib.parse
# Modify the links and data below:
DEADLINES_URL = "https://m3104.nawinds.dev/DEADLINES.json"
@ -15,88 +15,37 @@ BOT_NAME = "Дединсайдер M3104"
BOT_USERNAME = "m3104_deadliner_bot"
# Environment variables that should be available:
API_URL = 'https://api.telegram.org/bot'
TOKEN = os.getenv("TOKEN")
MAIN_GROUP_ID = int(os.getenv("MAIN_GROUP_ID") or '0')
EDIT_MESSAGE_ID = int(os.getenv("EDIT_MESSAGE_ID") or '0')
ADD_CALENDAR_LINK = os.getenv("ADD_CALENDAR_LINK") != 'false'
assert TOKEN, "Missing token!"
assert MAIN_GROUP_ID, "Missing group ID!"
MAIN_GROUP_ID = int(os.getenv("MAIN_GROUP_ID"))
logging.basicConfig(level=logging.INFO)
bot = Bot(TOKEN)
NUMBER_EMOJIS = ['0.', '1', '2', '3', '4', '5', '6', '7', '8', '9', '🔟']
class TelegramException(Exception):
def __init__(self, *, error_code: int, description: str, **_):
super().__init__(f'Error {error_code}: {description}')
self.error_code = error_code
self.description = description
def telegram_request(method: str, args: dict):
data = requests.post(API_URL + f'{TOKEN}/{method}', json=args).json()
if not data['ok']:
raise TelegramException(**data)
return data
def send_message(text: str) -> int:
return telegram_request('sendMessage', {
'chat_id': MAIN_GROUP_ID,
'parse_mode': 'HTML',
'text': text,
'link_preview_options': {
'is_disabled': True
}
})['result']['message_id']
def edit_message(message_id: int, text: str) -> int:
return telegram_request('editMessageText', {
'chat_id': MAIN_GROUP_ID,
'parse_mode': 'HTML',
'message_id': message_id,
'text': text,
'link_preview_options': {
'is_disabled': True
}
})['result']['message_id']
def delete_message(message_id: int) -> bool:
return telegram_request('deleteMessage', {
'chat_id': MAIN_GROUP_ID,
'message_id': message_id
})['result']
def get_current_time() -> str:
async def get_current_time() -> str:
current_time = dt.datetime.now()
current_time_hour = current_time.hour if current_time.hour >= 10 else "0" + str(current_time.hour)
current_time_minute = current_time.minute if current_time.minute >= 10 else "0" + str(current_time.minute)
return f"{current_time_hour}:{current_time_minute}"
def get_dt_obj_from_string(time: str) -> dt.datetime:
time = time.replace('GMT+3', '+0300')
locale.setlocale(locale.LC_TIME, 'en_US.UTF-8')
return dt.datetime.strptime(time, "%d %b %Y %H:%M:%S %z")
def generate_link(event_name: str, event_time: str) -> str:
async def generate_link(event_name: str, event_time: str) -> str:
dt_obj = get_dt_obj_from_string(event_time)
formatted_time = dt_obj.strftime("%Y%m%d T%H%M%S%z")
description = f"Дедлайн добавлен ботом {BOT_NAME} (https://t.me/{BOT_USERNAME})"
link = f"https://calendar.google.com/calendar/u/0/r/eventedit?" \
f"text={urllib.parse.quote(event_name)}&" \
f"dates={formatted_time}/{formatted_time}"
f"dates={formatted_time}/{formatted_time}&details={urllib.parse.quote(description)}&" \
f"color=6"
return link
def get_human_timedelta(time: str) -> str:
async def get_human_timedelta(time: str) -> str:
dt_obj = get_dt_obj_from_string(time)
dt_now = dt.datetime.now(dt_obj.tzinfo) # Ensure timezones are consistent
delta = dt_obj - dt_now
@ -115,133 +64,152 @@ def get_human_timedelta(time: str) -> str:
else:
return f"{hours}ч {minutes}м"
def get_human_time(time: str) -> str:
async def get_human_time(time: str) -> str:
dt_obj = get_dt_obj_from_string(time)
locale.setlocale(locale.LC_TIME, 'ru_RU.UTF-8')
formatted_date = dt_obj.strftime("%a, %d %B в %H:%M")
return formatted_date
def timestamp_func(a: dict) -> float:
time = a["time"].replace('GMT+3', '+0300')
locale.setlocale(locale.LC_TIME, 'en_US.UTF-8')
a_timestamp = dt.datetime.strptime(time, "%d %b %Y %H:%M:%S %z").timestamp()
return a_timestamp # 29 Oct 2024 23:59:59 GMT+3
def relevant_filter_func(d: dict) -> bool:
dt_obj = get_dt_obj_from_string(d["time"])
return not dt_obj < dt.datetime.now(dt_obj.tzinfo)
if dt_obj < dt.datetime.now(dt_obj.tzinfo):
return False
return True
def deadline_type_filter_func(d: dict, dtype: str) -> bool:
if f"[{dtype.lower()}]" in d["name"].lower():
return True
return False
def deadline_type_filter_func(d: dict, dtype: str = '') -> bool:
if not dtype:
return not re.match(r'^\[.*\]', d['name'])
def deadlines_filter_func(d: dict) -> bool:
return (not deadline_type_filter_func(d, "тест") and
not deadline_type_filter_func(d, "лекция"))
return f"[{dtype.lower()}]" in d["name"].lower()
def get_message_text() -> str:
async def get_message_text() -> str:
try:
response = requests.get(DEADLINES_URL).json()
except Exception as e:
print(f"{dt.datetime.now()} Failed to fetch deadlines: {e}")
print(f"{datetime.datetime.now()} Failed to fetch deadlines: {e}")
return ""
all_deadlines = response["deadlines"]
types = [
('', ''), # deadlines
('🧑‍💻 Тесты', 'тест'),
('🛡 Защиты', 'защита'),
('🎓 Лекции', 'лекция'),
('🤓 Экзамены', 'экзамен'),
('👞 Консультации', 'консультация'),
]
deadlines = list(filter(lambda d: deadlines_filter_func(d) and relevant_filter_func(d), all_deadlines))
tests = list(filter(lambda t: deadline_type_filter_func(t, "тест") and relevant_filter_func(t), all_deadlines))
lectures = list(filter(lambda t: deadline_type_filter_func(t, "лекция") and relevant_filter_func(t), all_deadlines))
assignments = [(sorted(filter(lambda t: deadline_type_filter_func(t, x[1]) and relevant_filter_func(t), all_deadlines),
key=lambda z: timestamp_func(z)), x[0], x[1]) for x in types]
text = f"🔥️️ <b>Дедлайны</b> (<i>Обновлено в {await get_current_time()} 🔄</i>):\n\n"
text = f"🔥️️ <b>Дедлайны</b> (<i>Обновлено в {get_current_time()} 🔄</i>):\n\n"
deadlines = sorted(deadlines, key=lambda x: timestamp_func(x))
tests = sorted(tests, key=lambda x: timestamp_func(x))
lectures = sorted(lectures, key=lambda x: timestamp_func(x))
if len(assignments[0]) == 0:
if len(deadlines) == 0:
text += "Дедлайнов нет)\n\n"
def add_items(items: list, category_name: str = '', replace_name: str = ''):
if len(items) == 0:
return
for i in range(len(deadlines)):
no = i + 1
if no < 11:
no = NUMBER_EMOJIS[no] + " "
else:
no += ". "
text += str(no) + "<b>"
nonlocal text
REPLACE_PATTERN = re.compile(rf'^\[{replace_name}\] ', flags=re.IGNORECASE)
if deadlines[i].get("url"):
text += f"<a href='{deadlines[i]['url']}'>{deadlines[i]['name']}</a>"
else:
text += deadlines[i]["name"]
if category_name:
text += f"\n<b>{category_name}</b>:\n\n"
text += "</b> — "
text += await get_human_timedelta(deadlines[i]["time"])
text += f"\n(<a href='{await generate_link(deadlines[i]['name'], deadlines[i]['time'])}'>"
text += await get_human_time(deadlines[i]["time"]) + "</a>)\n\n"
for i, item in enumerate(items):
if len(tests) > 0:
text += f"\n🧑‍💻 <b>Тесты</b>:\n\n"
for i in range(len(tests)):
test_name = tests[i]["name"].replace("[Тест] ", "").replace("[тест]", "")
test_url = tests[i].get("url")
no = i + 1
if no <= 10:
if no < 11:
no = NUMBER_EMOJIS[no] + " "
else:
no = str(no) + ". "
no += ". "
text += str(no) + "<b>"
text += no + "<b>"
name = re.sub(REPLACE_PATTERN, '', item['name'])
url = item.get('url')
if url:
text += f"<a href='{url}'>{name}</a>"
if test_url:
text += f"<a href='{test_url}'>{test_name}</a>"
else:
text += name
text += test_name
text += "</b> — "
text += get_human_timedelta(item["time"])
if ADD_CALENDAR_LINK:
text += f"\n(<a href='{generate_link(name, item['time'])}'>"
text += get_human_time(item["time"]) + "</a>)\n\n"
text += await get_human_timedelta(tests[i]["time"])
text += f"\n(<a href='{await generate_link(test_name, tests[i]['time'])}'>"
text += await get_human_time(tests[i]["time"]) + "</a>)\n\n"
if len(lectures) > 0:
text += f"\n👨‍🏫 <b>Лекции</b>:\n\n"
for i in range(len(lectures)):
lecture_name = lectures[i]["name"].replace("[Лекция] ", "").replace("[лекция]", "")
lecture_url = lectures[i].get("url")
no = i + 1
if no < 11:
no = NUMBER_EMOJIS[no] + " "
else:
text += f'\n({get_human_time(item["time"])})\n\n'
no += ". "
text += str(no) + "<b>"
for assignment_type in assignments:
add_items(*assignment_type)
if lecture_url:
text += f"<a href='{lecture_url}'>{lecture_name}</a>"
else:
text += lecture_name
text += (
f"\n🆕 <a href='{ADD_DEADLINE_LINK}'>"
f"Добавить дедлайн</a>"
)
text += "</b> — "
text += await get_human_timedelta(lectures[i]["time"])
text += f"\n(<a href='{await generate_link(lecture_name, lectures[i]['time'])}'>"
text += await get_human_time(lectures[i]["time"]) + "</a>)\n\n"
text += f"\n🆕 <a href='{ADD_DEADLINE_LINK}'>" \
f"Добавить дедлайн</a>"
return text
async def send_deadlines(chat_id: int) -> None:
text = await get_message_text()
if text == "Дедлайнов нет)\n\n":
return
def main() -> None:
text = get_message_text()
if EDIT_MESSAGE_ID:
msg_id = edit_message(EDIT_MESSAGE_ID, text)
else:
msg_id = send_message(text)
msg = await bot.send_message(chat_id, text, parse_mode="HTML", disable_web_page_preview=True)
started_updating = dt.datetime.now()
print(dt.datetime.now(), "Message sent. Msg id:", msg_id)
print(datetime.datetime.now(), "Message sent. Msg id:", msg.message_id)
condition = (lambda: True) if EDIT_MESSAGE_ID else (lambda: dt.datetime.now() - started_updating < dt.timedelta(days=1))
while condition():
time.sleep(60)
while dt.datetime.now() - started_updating < dt.timedelta(days=1):
await asyncio.sleep(60)
try:
new_text = get_message_text()
new_text = await get_message_text()
if text != new_text and new_text != "":
edit_message(msg_id, new_text)
await msg.edit_text(new_text, parse_mode="HTML", disable_web_page_preview=True)
text = new_text
print(dt.datetime.now(), "Message updated. Msg id:", msg_id)
print(datetime.datetime.now(), "Message updated. Msg id:", msg.message_id)
else:
print(dt.datetime.now(), "Message update skipped. Msg id:", msg_id)
print(datetime.datetime.now(), "Message update skipped. Msg id:", msg.message_id)
except Exception as e:
logging.warning(dt.datetime.now(),f"{dt.datetime.now()} Error updating message: {e}")
logging.warning(datetime.datetime.now(),f"{datetime.datetime.now()} Error updating message: {e}")
continue
await msg.delete()
if not EDIT_MESSAGE_ID:
delete_message(msg_id)
async def main():
await send_deadlines(MAIN_GROUP_ID)
await bot.session.close()
if __name__ == '__main__':
main()
asyncio.run(main())

View file

@ -1,5 +1,21 @@
certifi==2025.4.26
charset-normalizer==3.4.2
aiofiles==24.1.0
aiogram==3.13.1
aiohappyeyeballs==2.4.3
aiohttp==3.10.10
aiosignal==1.3.1
annotated-types==0.7.0
async-timeout==4.0.3
attrs==24.2.0
certifi==2024.8.30
charset-normalizer==3.4.0
frozenlist==1.4.1
idna==3.10
magic-filter==1.0.12
multidict==6.1.0
propcache==0.2.0
pydantic==2.9.2
pydantic_core==2.23.4
requests==2.32.3
urllib3==2.4.0
typing_extensions==4.12.2
urllib3==2.2.3
yarl==1.15.5