1
0
Fork 0
mirror of https://github.com/EDeev/y.calendarkin.git synced 2026-10-08 00:39:27 +03:00
y.calendarkin/code/bot.py
Egor Deev 15bd36580e Исправлены ошибки и уязвимость, улучшены уведомления
Ошибки:
- исключение в фоновых циклах (сбой сети при обновлении календаря, заблокировавший бота
  пользователь, битый календарь) останавливало весь бот — теперь ошибка у одного
  пользователя логируется и не мешает остальным;
- повторяющиеся события с UNTIL местным временем или датой роняли разбор календаря
  (ValueError из dateutil) — UNTIL приводится к UTC;
- названия и описания событий подставлялись в HTML без экранирования: «<» или «&» ломали
  отправку уведомления;
- сообщение до /start и кривой ввод времени напоминаний роняли обработчик;
- при отсутствии файла календаря разбор падал.

Безопасность: ссылка скачивалась без проверки (wget), бота можно было заставить ходить по
любым адресам, включая внутренние. Теперь принимается только https://calendar.yandex.*,
скачивание — через aiohttp с таймаутом и без редиректов, содержимое проверяется как iCal
до замены старого файла.

Уведомления (по issue #3): без даты и года — только время, пустые описание и организатор
не показываются, организатор без «mailto:», для событий на весь день — «Весь день» и без
напоминаний в 00:01; новые команды /link и /unsubscribe.

Прочее: токен из BOT_TOKEN, таблицы SQLite создаются при первом запуске (пустые базы
убраны из репозитория), ссылка «АВТОР» — на EDeev вместо чужого теперь профиля IGlek.
2026-10-05 19:28:43 +00:00

113 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import logging
from datetime import datetime, time
from init import bot, dp, du, dc
from script import delta_time, download_ical, dt_now, message_form, text_ical
from handlers import router
async def send(tg_id, text):
await bot.send_message(chat_id=tg_id, disable_web_page_preview=True, text=text)
async def check_user(user_id):
events = sorted(text_ical(user_id, du.get_tz(user_id)), key=lambda e: (e[0], e[1], e[2]))
tg_id = str(du.get_first_user_id(user_id))
start = dc.get_start(user_id)
daily = dc.get_daily(user_id)
alarm = dc.get_alarm(user_id)
alarm2_status = dc.get_status2(user_id)
today = dt_now(du.get_tz(user_id)).date()
time_check = dt_now(du.get_tz(user_id)).time()
# ДЛЯ DAILY
counter = 0
txt = "<b>События сегодня</b>\n\n"
delta_daily1 = time(hour=8, minute=0)
delta_daily2 = time(hour=8, minute=1)
# --------------------------------
for event in events:
if event[0] == today:
d_event = datetime.combine(today, event[1])
if daily and delta_daily1 <= time_check < delta_daily2:
counter += 1
txt += message_form(counter, event[3])
# для событий на весь день напоминания по времени не шлём
if event[3]["all_day"]:
continue
delta_start = delta_time(d_event, 1, 0)
if start and delta_start[0] < time_check <= delta_start[1]:
await send(tg_id, "<b>Событие начинается!</b>\n\n" + message_form(0, event[3]))
delta_alarm1 = delta_time(d_event, alarm[0], alarm[0] - 1)
if delta_alarm1[0] < time_check <= delta_alarm1[1]:
await send(tg_id, f"<b>Напоминаю!</b>\n<i>Через {alarm[0]} минут "
f"будет событие:</i>\n\n{message_form(0, event[3])}")
if alarm2_status:
delta_alarm2 = delta_time(d_event, alarm[1], alarm[1] - 1)
if delta_alarm2[0] < time_check <= delta_alarm2[1]:
await send(tg_id, f"<b>Напоминаю!</b>\n<i>Через {alarm[1]} минут "
f"будет событие:</i>\n\n{message_form(0, event[3])}")
if daily and delta_daily1 <= time_check < delta_daily2:
if counter:
await send(tg_id, txt)
else:
await send(tg_id, "Сегодня событий <b>нет</b>")
# ПРОВЕРКА НА СОБЫТИЕ
async def alarm(wait_for):
while True:
await asyncio.sleep(wait_for)
for (user_id,) in du.all_users():
# ошибка у одного пользователя (заблокировал бота, битый календарь) не должна
# останавливать рассылку остальным и весь бот
try:
if dc.clock_exists(user_id) and du.get_status(user_id):
await check_user(user_id)
except Exception:
logging.exception("Ошибка проверки событий пользователя %s", user_id)
async def update(wait_for):
while True:
await asyncio.sleep(wait_for)
for (user_id,) in du.all_users():
try:
if du.url_exists(user_id) and du.get_status(user_id):
await download_ical(du.get_url(user_id), user_id)
except Exception as err:
# при сбое остаётся прошлая версия календаря
logging.warning("Не удалось обновить календарь пользователя %s: %s", user_id, err)
async def main() -> None:
dp.include_router(router)
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
async def process() -> None:
await asyncio.gather(main(), # ЗАПУСК ОСНОВНОЙ ПРОГРАММЫ
alarm(60), # ПРОВЕРКА КАЖДУЮ 1 МИНУТУ
update(780)) # ПРОВЕРКА КАЖДУЮ 13 МИНУТУ
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
try: asyncio.run(process())
except KeyboardInterrupt: pass