diff --git a/code/bot.py b/code/bot.py new file mode 100644 index 0000000..27c850e --- /dev/null +++ b/code/bot.py @@ -0,0 +1,318 @@ +import logging, config, sql, asyncio, wget, os + +from aiogram import Bot, Dispatcher, executor, types +from aiogram.types import ParseMode + +from datetime import datetime, time +from script import text_ical, message_form, dt_now, delta_time + +# инициализируем токен +logging.basicConfig(level=logging.INFO) + +bot = Bot(token=config.TOKEN) +dp = Dispatcher(bot) + +# инициализируем соединение с БД +du = sql.Users('../db/users.db') +dc = sql.Clock('../db/clock.db') + + +# ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ +@dp.message_handler(commands=['start', 'help']) +async def helps(message: types.Message): + tg_id = int(message.chat.id) + if not du.user_exists(tg_id): + du.add_user(tg_id) + + buttons = [types.InlineKeyboardButton(text="КОМАНДЫ", callback_data="com"), + types.InlineKeyboardButton(text="АВТОР", callback_data="auth")] + + keyboard = types.InlineKeyboardMarkup(row_width=2) + keyboard.add(*buttons) + + await message.answer(text="Я.Календаркин - бот для оповещения о событиях из Яндекс.Календаря. " + "Для начала работы вам нужно всего лишь прислать в чат ссылку экспорта календаря в " + "формате ICal. После получения ссылки, бот начнёт оповещать о всех новых событиях " + "и появится возможность настройки оповещений. О том, какие команды есть для настройки, " + "вы можете ознакомиться по кнопке КОМАНДЫ", + parse_mode=ParseMode.HTML, reply_markup=keyboard) + + +@dp.callback_query_handler(text="auth") +async def author(call: types.CallbackQuery): + await call.message.answer(text='*| АВТОР |*\n\n*>>* Этот бот не коммерческий проект, для упрощенного получения ' + 'уведомлений о событиях в Яндекс.Календаре. Не многим этот бот будет полезен, но ' + 'людям, чья работа подразумевает его использование, он станет лишь удобным ' + 'инструментом. Я же пишу подобные небольшие проекты, о которых вы можете узнать ' + 'больше на моём [GitHub](https://github.com/IGlek).', + parse_mode=ParseMode.MARKDOWN) + + +@dp.callback_query_handler(text="com") +async def commands(call: types.CallbackQuery): + await call.message.answer(text='| КОМАНДЫ |\n\n' + '/help - вспомогательная функция для уточнения работы команд\n' + '/list - список событий календаря, запланированных на сегодняшний день\n' + '/notif - команда, отключающая рассылку уведомлений, даже при наличии событий в календаре\n' + '/daily - оповещение в 8 утра по вашему часовому поясу со списком событий на день\n' + '/moment - напоминание, приходящее в момент начала события\n\n' + '/get_alarm - информация о времени на которое настроены оповещения\n' + '/edit_alarm - изменение времени оповещений\n' + '/stop_alarm - команда, отключающая второе оповещение о событии', + parse_mode=types.ParseMode.HTML) + + +# КОМАНДЫ +@dp.message_handler(commands=['list']) +async def check_list(message: types.Message): + user_id = du.get_user_id(int(message.chat.id)) + + if du.url_exists(user_id): + txt = "Имеющиеся события на сегодня\n\n" + + lst_events = sorted(text_ical(user_id, du.get_tz(user_id))) + today = dt_now(du.get_tz(user_id)).date() + + counter = 0 + for event in lst_events: + if event[0] == today: + counter += 1 + txt += message_form(counter, event[3]) + + await message.answer(text=txt, parse_mode=ParseMode.HTML) + else: + await message.answer("Для отображения событий вы должны прислать ical-ссылку на календарь!") + + +@dp.message_handler(commands=['notif']) +async def notif_up(message: types.Message): + user_id = du.get_user_id(int(message.chat.id)) + + if du.url_exists(user_id): + if du.get_status(user_id): + await message.answer("Уведомления о событиях выключены!") + else: + await message.answer("Уведомления о событиях включены!") + + du.update_status(user_id) + else: + await message.answer("Для взаимодействия с событиями вы должны прислать ical-ссылку на календарь!") + + +@dp.message_handler(commands=['daily']) +async def daily_up(message: types.Message): + user_id = du.get_user_id(int(message.chat.id)) + + if dc.clock_exists(user_id): + if dc.get_daily(user_id): + await message.answer("Ежедневные утренние уведомления выключены!") + else: + await message.answer("Ежедневные утренние уведомления включены!") + + dc.update_daily(user_id) + else: + await message.answer("Для взаимодействия с событиями вы должны прислать ical-ссылку на календарь!") + + +@dp.message_handler(commands=['moment']) +async def start_up(message: types.Message): + user_id = du.get_user_id(int(message.chat.id)) + + if dc.clock_exists(user_id): + if dc.get_start(user_id): + await message.answer("Уведомления в момент события выключены!") + else: + await message.answer("Уведомления в момент события включены!") + + dc.update_start(user_id) + else: + await message.answer("Для взаимодействия с событиями вы должны прислать ical-ссылку на календарь!") + + +@dp.message_handler(commands=['get_alarm']) +async def start_up(message: types.Message): + user_id = du.get_user_id(int(message.chat.id)) + + if dc.clock_exists(user_id): + alarms = dc.get_alarm(user_id) + start = dc.get_start(user_id) + status2 = dc.get_status2(user_id) + + txt = "" + + if status2: + txt += "У вас работает два оповещения" + else: + txt += "У вас работает лишь первое оповещение" + + if start: + txt += " и сообщение в момент начала события!" + else: + txt += "!" + + await message.answer(text=(txt + f"\n\nПервое оповещение приходит за {alarms[0]} минут\n" + f"Второе оповещение приходит за {alarms[1]} минут"), + parse_mode=ParseMode.HTML) + else: + await message.answer("Для того, чтобы получить таймеры, вы должны прислать ical-ссылку на свой календарь!") + + +@dp.message_handler(commands=['edit_alarm']) +async def start_up(message: types.Message): + user_id = du.get_user_id(int(message.chat.id)) + + if dc.clock_exists(user_id): + await message.bot.send_photo(chat_id=message.chat.id, photo=open("../data/photo_edit_alarm.jpg", "rb"), + caption="Для изменения времени вам надо в ответ на это сообщение прислать два " + "числа через пробел: разница времени первого и второго таймера по ходу " + "времени соответственно") + else: + await message.answer("Для того, чтобы изменить таймеры, вы должны прислать ical-ссылку на свой календарь!") + + +@dp.message_handler(commands=['stop_alarm']) +async def daily_up(message: types.Message): + user_id = du.get_user_id(int(message.chat.id)) + + if dc.clock_exists(user_id): + if dc.get_status2(user_id): + await message.answer("Второе уведомление выключено!") + else: + await message.answer("Второе уведомление включено!") + + dc.update_status2(user_id) + else: + await message.answer("Для взаимодействия с событиями вы должны прислать ical-ссылку на календарь!") + + +# ЗАГРУЗКА ССЫЛКИ +@dp.message_handler(content_types=['text']) +async def downloading_file_ics(message: types.Message): + user_id = du.get_user_id(int(message.chat.id)) + + if message.text[:5] == "https": + try: + wget.download(message.text, f'../data/icals/{str(user_id)}_new.ics') + + try: + os.remove(f'../data/icals/{str(user_id)}.ics') + except FileNotFoundError: + pass + + os.rename(f'../data/icals/{str(user_id)}_new.ics', f'../data/icals/{str(user_id)}.ics') + + time_zone = message.text.split("=")[-1] + if not du.url_exists(user_id): + du.add_url(user_id, message.text, time_zone) + dc.add_clock(user_id) + else: + du.update_url(user_id, message.text, time_zone) + + await message.answer("Ссылка успешно добавлена! Уведомления уже включены!") + except Exception: + await message.answer("Ошибка скачивания! Проверьте правильность ссылки и пришлите ещё раз") + + if 'reply_to_message' in message and dc.clock_exists(user_id): + text = "Для изменения времени вам надо в ответ на это сообщение прислать два числа через " \ + "пробел: разница времени первого и второго таймера по ходу времени соответственно" + + if message.reply_to_message.caption == text: + alarm_new = message.text.split() + + if int(alarm_new[0]) < 60 and int(alarm_new[1]) < 60: + dc.update_alarm1(user_id, int(alarm_new[0])) + dc.update_alarm2(user_id, int(alarm_new[1])) + + await message.answer("Время отправки уведомлений успешно обновлено!") + else: + await message.answer("Время отправки уведомлений должно быть меньше 60 минут!") + + + +# ПРОВЕРКА НА СОБЫТИЕ +async def alarm(wait_for): + while True: + await asyncio.sleep(wait_for) + + users_id = du.all_users() + for user_id in users_id: + user_id = user_id[0] + + if dc.clock_exists(user_id): + if du.get_status(user_id): + events = sorted(text_ical(user_id, du.get_tz(user_id))) + 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 = "События сегодня\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]) + + delta_start = delta_time(d_event, 1, 0) + if start and delta_start[0] < time_check <= delta_start[1]: + await bot.send_message(chat_id=tg_id, parse_mode=ParseMode.HTML, + text=f"Событие начинается!\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 bot.send_message(chat_id=tg_id, parse_mode=ParseMode.HTML, + text=f"Напоминаю!\nЧерез {alarm[0]} минут " + f"будет событие:\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 bot.send_message(chat_id=tg_id, parse_mode=ParseMode.HTML, + text=f"Напоминаю!\nЧерез {alarm[1]} минут " + f"будет событие:\n\n{message_form(0, event[3])}") + + if daily and delta_daily1 <= time_check < delta_daily2: + if counter: + await bot.send_message(chat_id=tg_id, parse_mode=ParseMode.HTML, + text=txt) + else: + await bot.send_message(chat_id=tg_id, parse_mode=ParseMode.HTML, + text=f"Сегодня событий нет") + + +async def update(wait_for): + while True: + await asyncio.sleep(wait_for) + + users_id = du.all_users() + for user_id in users_id: + user_id = user_id[0] + + if du.url_exists(user_id): + if du.get_status(user_id): + wget.download(du.get_url(user_id), f'../data/icals/{str(user_id)}_new.ics') + + os.remove(f'../data/icals/{str(user_id)}.ics') + os.rename(f'../data/icals/{str(user_id)}_new.ics', f'../data/icals/{str(user_id)}.ics') + + +if __name__ == '__main__': + loop = asyncio.get_event_loop() + loop.create_task(alarm(60)) # ПРОВЕРКА КАЖДУЮ 1 МИНУТУ + loop.create_task(update(1620)) # ПРОВЕРКА КАЖДУЮ 27 МИНУТУ + executor.start_polling(dp, skip_updates=True) diff --git a/code/config.py b/code/config.py new file mode 100644 index 0000000..1cf651a --- /dev/null +++ b/code/config.py @@ -0,0 +1 @@ +TOKEN = "**********:***************************" diff --git a/code/script.py b/code/script.py new file mode 100644 index 0000000..72c1baf --- /dev/null +++ b/code/script.py @@ -0,0 +1,68 @@ +from datetime import datetime, time +import icalendar + + +def text_ical(user_id, tz): + date = dt_now(tz) + path = f'../data/icals/{user_id}.ics' + + e = open(path, 'rb') + + ecal = icalendar.Calendar.from_ical(e.read()) + events = [] + + for i, component in enumerate(ecal.walk()): + if component.name == "VEVENT": + + dt_start = component.decoded("dtstart") + dt_start.replace(tzinfo=None) + dt_start = dt_start.astimezone(tz) + + dt_end = component.decoded("dtend") + dt_end.replace(tzinfo=None) + dt_end = dt_end.astimezone(tz) + + if date.date() == component.decoded("dtstart").date(): + org = component.get("organizer") + desc = component.get("description") + + event = {"name": component.get('summary'), "desc": desc if desc else "отсутствует", + "org": org if org else "не назначен", "datetime": [dt_start, dt_end]} + + events.append([dt_start.date(), dt_start.time(), i, event]) + e.close() + + return events + + +def message_form(k, event): + txt = "" + + if k: + txt += f"-------\n" + txt += f"{k}. {event['name']}\n" + else: + txt += f"{event['name']}\n" + + txt += f"Описание: {event['desc']}\n" + txt += f"Организатор: {event['org']}\n\n" + txt += f"Начало: {event['datetime'][0].strftime('%H:%M - %d.%m.%Y года')}\n" + txt += f"Конец: {event['datetime'][1].strftime('%H:%M - %d.%m.%Y года')}\n" + + return txt + + +def delta_time(d_event, start, end): + d_start = datetime.combine(d_event.date(), time(hour=0, minute=start)) + d_end = datetime.combine(d_event.date(), time(hour=0, minute=end)) + + zero = datetime.combine(d_event.date(), time(0, 0, 0, 0)) + + dt_start = zero + (d_event - d_start) + dt_end = zero + (d_event - d_end) + + return [dt_start.time(), dt_end.time()] + + +def dt_now(tz): + return datetime.now(tz=tz) diff --git a/code/sql.py b/code/sql.py new file mode 100644 index 0000000..87ef9c2 --- /dev/null +++ b/code/sql.py @@ -0,0 +1,159 @@ +from pytz import timezone +import sqlite3 + + +class Users: + def __init__(self, database): + """Подключаемся к БД и сохраняем курсор соединения""" + self.connection = sqlite3.connect(database) + self.cursor = self.connection.cursor() + + # КОМАНДЫ USER + def user_exists(self, user_id): + """Проверяем, есть ли уже пользователь в базе""" + with self.connection: + result = self.cursor.execute(f'SELECT * FROM `user` WHERE `user_id` = ?', (user_id,)).fetchall() + return bool(len(result)) + + def all_users(self): + """Список айди""" + with self.connection: + return self.cursor.execute(f'SELECT `id` FROM `user`').fetchall() + + def add_user(self, user_id): + """Добавляем нового пользователя""" + with self.connection: + return self.cursor.execute(f"INSERT INTO `user` (`user_id`) VALUES(?)", (user_id,)) + + def get_user_id(self, user_id): + """Получаем короткое айди юзера""" + with self.connection: + return self.cursor.execute(f'SELECT `id` FROM `user` WHERE `user_id` = ?', (user_id,)).fetchone()[0] + + def get_first_user_id(self, user_id): + """Получаем длинное айди юзера""" + with self.connection: + return self.cursor.execute(f'SELECT `user_id` FROM `user` WHERE `id` = ?', (user_id,)).fetchone()[0] + + # КОМАНДЫ URL + def url_exists(self, user_id): + """Проверяем, есть ли данные уже в базе""" + with self.connection: + result = self.cursor.execute(f'SELECT * FROM `url` WHERE `user_id` = ?', (user_id,)).fetchall() + return bool(len(result)) + + def add_url(self, user_id, url_ical, time_zone): + """Добавляем ссылку на календарь""" + with self.connection: + return self.cursor.execute(f"INSERT INTO `url` (`user_id`, `url_ical`, `time_zone`) VALUES(?, ?, ?)", + (user_id, url_ical, time_zone)) + + def update_status(self, user_id): + """Обновляем статус рассылки уведомлений""" + with self.connection: + status = self.cursor.execute(f'SELECT `status` FROM `url` WHERE `user_id` = ?', (user_id,)).fetchone()[0] + return self.cursor.execute("UPDATE `url` SET `status` = ? WHERE `user_id` = ?", (not status, user_id)) + + def update_url(self, user_id, url_ical, time_zone): + """Обновляем ссылку и часовой пояс в базе""" + with self.connection: + return self.cursor.execute("UPDATE `url` SET `url_ical` = ?, `time_zone` = ? WHERE `user_id` = ?", + (url_ical, time_zone, user_id)) + + def get_status(self, user_id): + """Получаем статус работы""" + with self.connection: + return self.cursor.execute(f'SELECT `status` FROM `url` WHERE `user_id` = ?', (user_id,)).fetchone()[0] + + def get_url(self, user_id): + """Получаем ссылку на календарь""" + with self.connection: + return self.cursor.execute(f'SELECT `url_ical` FROM `url` WHERE `user_id` = ?', (user_id,)).fetchone()[0] + + def get_tz(self, user_id): + """Получаем указанный в календаре часовой пояс""" + with self.connection: + return timezone(self.cursor.execute(f'SELECT `time_zone` FROM `url` WHERE `user_id` = ?', + (user_id,)).fetchone()[0]) + + # ЗАКРЫТИЕ ВЫЗОВА + def close(self): + """Закрываем соединение с БД""" + self.connection.close() + + +class Clock: + def __init__(self, database): + """Подключаемся к БД и сохраняем курсор соединения""" + self.connection = sqlite3.connect(database) + self.cursor = self.connection.cursor() + + # КОМАНДЫ ALARM + def clock_exists(self, user_id): + """Проверяем, есть ли данные уже в базе""" + with self.connection: + result = self.cursor.execute(f'SELECT * FROM `alarm` WHERE `user_id` = ?', (user_id,)).fetchall() + return bool(len(result)) + + def add_clock(self, user_id): + """Добавляем параметры уведомления""" + with self.connection: + return self.cursor.execute(f"INSERT INTO `alarm` (`user_id`, `alarm_1`, `alarm_2`) VALUES(?, ?, ?)", + (user_id, 15, 5)) + + # КОМАНДЫ ПОЛУЧЕНИЯ ССЫЛОК + def get_alarm(self, user_id): + """Получаем задержки таймера""" + with self.connection: + return self.cursor.execute(f'SELECT `alarm_1`, `alarm_2` FROM `alarm` WHERE `user_id` = ?', + (user_id,)).fetchone() + + def get_start(self, user_id): + """Получаем статус уведомления в момент исполнения""" + with self.connection: + return self.cursor.execute(f'SELECT `start` FROM `alarm` WHERE `user_id` = ?', (user_id,)).fetchone()[0] + + def get_daily(self, user_id): + """Получаем статус ежедневного уведомления""" + with self.connection: + return self.cursor.execute(f'SELECT `daily` FROM `alarm` WHERE `user_id` = ?', (user_id,)).fetchone()[0] + + def get_status2(self, user_id): + """Получаем статус отправки второго уведомления""" + with self.connection: + return self.cursor.execute(f'SELECT `status_2` FROM `alarm` WHERE `user_id` = ?', (user_id,)).fetchone()[0] + + # КОМАНДЫ ОБНОВЛЕНИЯ БУЛЕВЫХ СТАТУСОВ + def update_daily(self, user_id): + """Обновляем статус ежедневного уведомления""" + with self.connection: + daily = self.cursor.execute(f'SELECT `daily` FROM `alarm` WHERE `user_id` = ?', (user_id,)).fetchone()[0] + return self.cursor.execute("UPDATE `alarm` SET `daily` = ? WHERE `user_id` = ?", (not daily, user_id)) + + def update_start(self, user_id): + """Обновляем статус уведомления в момент исполнения""" + with self.connection: + start = self.cursor.execute(f'SELECT `start` FROM `alarm` WHERE `user_id` = ?', (user_id,)).fetchone()[0] + return self.cursor.execute("UPDATE `alarm` SET `start` = ? WHERE `user_id` = ?", (not start, user_id)) + + # КОМАНДЫ ОБНОВЛЕНИЯ ВРЕМЕННОГО ДИАПАЗОНА + def update_alarm1(self, user_id, alarm_1): + """Обновляем время первого оповещения""" + with self.connection: + return self.cursor.execute("UPDATE `alarm` SET `alarm_1` = ? WHERE `user_id` = ?", (alarm_1, user_id)) + + def update_alarm2(self, user_id, alarm_2): + """Обновляем время второго оповещения""" + with self.connection: + return self.cursor.execute("UPDATE `alarm` SET `alarm_2` = ? WHERE `user_id` = ?", (alarm_2, user_id)) + + def update_status2(self, user_id): + """Обновляем статус отправки второго уведомления""" + with self.connection: + status_2 = self.cursor.execute(f'SELECT `status_2` FROM `alarm` WHERE `user_id` = ?', (user_id,)).fetchone()[0] + return self.cursor.execute("UPDATE `alarm` SET `status_2` = ? WHERE `user_id` = ?", (not status_2, user_id)) + + # ЗАКРЫТИЕ ВЫЗОВА + def close(self): + """Закрываем соединение с БД""" + self.connection.close() diff --git a/data/icals/1.ics b/data/icals/1.ics new file mode 100644 index 0000000..06a840e --- /dev/null +++ b/data/icals/1.ics @@ -0,0 +1,386 @@ +BEGIN:VCALENDAR +PRODID:-//Yandex LLC//Yandex Calendar//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-TIMEZONE:Europe/Samara +X-WR-CALNAME:Привет +BEGIN:VTIMEZONE +TZID:Asia/Krasnoyarsk +TZURL:http://tzurl.org/zoneinfo/Asia/Krasnoyarsk +X-LIC-LOCATION:Asia/Krasnoyarsk +BEGIN:STANDARD +TZOFFSETFROM:+061126 +TZOFFSETTO:+0600 +TZNAME:+06 +DTSTART:19200106T000000 +RDATE:19200106T000000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETFROM:+0600 +TZOFFSETTO:+0700 +TZNAME:+07 +DTSTART:19300621T000000 +RDATE:19300621T000000 +RDATE:19920119T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+0700 +TZOFFSETTO:+0800 +TZNAME:+08 +DTSTART:19810401T000000 +RDATE:19810401T000000 +RDATE:19820401T000000 +RDATE:19830401T000000 +RDATE:19840401T000000 +RDATE:19850331T020000 +RDATE:19860330T020000 +RDATE:19870329T020000 +RDATE:19880327T020000 +RDATE:19890326T020000 +RDATE:19900325T020000 +RDATE:19920329T020000 +RDATE:19930328T020000 +RDATE:19940327T020000 +RDATE:19950326T020000 +RDATE:19960331T020000 +RDATE:19970330T020000 +RDATE:19980329T020000 +RDATE:19990328T020000 +RDATE:20000326T020000 +RDATE:20010325T020000 +RDATE:20020331T020000 +RDATE:20030330T020000 +RDATE:20040328T020000 +RDATE:20050327T020000 +RDATE:20060326T020000 +RDATE:20070325T020000 +RDATE:20080330T020000 +RDATE:20090329T020000 +RDATE:20100328T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0800 +TZOFFSETTO:+0700 +TZNAME:+07 +DTSTART:19811001T000000 +RDATE:19811001T000000 +RDATE:19821001T000000 +RDATE:19831001T000000 +RDATE:19840930T030000 +RDATE:19850929T030000 +RDATE:19860928T030000 +RDATE:19870927T030000 +RDATE:19880925T030000 +RDATE:19890924T030000 +RDATE:19900930T030000 +RDATE:19920927T030000 +RDATE:19930926T030000 +RDATE:19940925T030000 +RDATE:19950924T030000 +RDATE:19961027T030000 +RDATE:19971026T030000 +RDATE:19981025T030000 +RDATE:19991031T030000 +RDATE:20001029T030000 +RDATE:20011028T030000 +RDATE:20021027T030000 +RDATE:20031026T030000 +RDATE:20041031T030000 +RDATE:20051030T030000 +RDATE:20061029T030000 +RDATE:20071028T030000 +RDATE:20081026T030000 +RDATE:20091025T030000 +RDATE:20101031T030000 +RDATE:20141026T020000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+0700 +TZOFFSETTO:+0700 +TZNAME:+07 +DTSTART:19910331T020000 +RDATE:19910331T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0700 +TZOFFSETTO:+0600 +TZNAME:+06 +DTSTART:19910929T030000 +RDATE:19910929T030000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETFROM:+0700 +TZOFFSETTO:+0800 +TZNAME:+08 +DTSTART:20110327T020000 +RDATE:20110327T020000 +END:STANDARD +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:Europe/Samara +TZURL:http://tzurl.org/zoneinfo/Europe/Samara +X-LIC-LOCATION:Europe/Samara +BEGIN:STANDARD +TZOFFSETFROM:+032020 +TZOFFSETTO:+0300 +TZNAME:+03 +DTSTART:19190701T032020 +RDATE:19190701T032020 +END:STANDARD +BEGIN:STANDARD +TZOFFSETFROM:+0300 +TZOFFSETTO:+0400 +TZNAME:+04 +DTSTART:19300621T000000 +RDATE:19300621T000000 +RDATE:19911020T030000 +RDATE:20110327T020000 +END:STANDARD +BEGIN:STANDARD +TZOFFSETFROM:+0400 +TZOFFSETTO:+0400 +TZNAME:+04 +DTSTART:19350127T000000 +RDATE:19350127T000000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+0400 +TZOFFSETTO:+0500 +TZNAME:+05 +DTSTART:19810401T000000 +RDATE:19810401T000000 +RDATE:19820401T000000 +RDATE:19830401T000000 +RDATE:19840401T000000 +RDATE:19850331T020000 +RDATE:19860330T020000 +RDATE:19870329T020000 +RDATE:19880327T020000 +RDATE:19920329T020000 +RDATE:19930328T020000 +RDATE:19940327T020000 +RDATE:19950326T020000 +RDATE:19960331T020000 +RDATE:19970330T020000 +RDATE:19980329T020000 +RDATE:19990328T020000 +RDATE:20000326T020000 +RDATE:20010325T020000 +RDATE:20020331T020000 +RDATE:20030330T020000 +RDATE:20040328T020000 +RDATE:20050327T020000 +RDATE:20060326T020000 +RDATE:20070325T020000 +RDATE:20080330T020000 +RDATE:20090329T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0500 +TZOFFSETTO:+0400 +TZNAME:+04 +DTSTART:19811001T000000 +RDATE:19811001T000000 +RDATE:19821001T000000 +RDATE:19831001T000000 +RDATE:19840930T030000 +RDATE:19850929T030000 +RDATE:19860928T030000 +RDATE:19870927T030000 +RDATE:19880925T030000 +RDATE:19920927T030000 +RDATE:19930926T030000 +RDATE:19940925T030000 +RDATE:19950924T030000 +RDATE:19961027T030000 +RDATE:19971026T030000 +RDATE:19981025T030000 +RDATE:19991031T030000 +RDATE:20001029T030000 +RDATE:20011028T030000 +RDATE:20021027T030000 +RDATE:20031026T030000 +RDATE:20041031T030000 +RDATE:20051030T030000 +RDATE:20061029T030000 +RDATE:20071028T030000 +RDATE:20081026T030000 +RDATE:20091025T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+0400 +TZOFFSETTO:+0400 +TZNAME:+04 +DTSTART:19890326T020000 +RDATE:19890326T020000 +RDATE:20100328T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0400 +TZOFFSETTO:+0300 +TZNAME:+03 +DTSTART:19890924T030000 +RDATE:19890924T030000 +RDATE:19900930T030000 +RDATE:20101031T030000 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+0300 +TZOFFSETTO:+0400 +TZNAME:+04 +DTSTART:19900325T020000 +RDATE:19900325T020000 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZOFFSETFROM:+0300 +TZOFFSETTO:+0300 +TZNAME:+03 +DTSTART:19910331T020000 +RDATE:19910331T020000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0300 +TZOFFSETTO:+0300 +TZNAME:+03 +DTSTART:19910929T030000 +RDATE:19910929T030000 +END:STANDARD +BEGIN:STANDARD +TZNAME:+04 +TZOFFSETFROM:+0400 +TZOFFSETTO:+0400 +DTSTART:20101031T030000 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Asia/Krasnoyarsk:20240123T213000 +DTEND;TZID=Asia/Krasnoyarsk:20240123T220000 +SUMMARY:Собрание +UID:zwdqywax0s4lz9ybyr9yandex.ru +SEQUENCE:0 +DTSTAMP:20240126T105030Z +CREATED:20240123T110608Z +DESCRIPTION:Очень важное +URL:https://calendar.yandex.ru/event?event_id=1980777684 +TRANSP:OPAQUE +CATEGORIES:Привет +LAST-MODIFIED:20240123T110608Z +CLASS:PRIVATE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Asia/Krasnoyarsk:20240123T203000 +DTEND;TZID=Asia/Krasnoyarsk:20240123T210000 +SUMMARY:Без названия +UID:zwdqywax0sz8640h6m8yandex.ru +SEQUENCE:0 +DTSTAMP:20240126T105030Z +CREATED:20240123T110935Z +URL:https://calendar.yandex.ru/event?event_id=1980779802 +TRANSP:OPAQUE +CATEGORIES:Привет +LAST-MODIFIED:20240123T110935Z +CLASS:PRIVATE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Asia/Krasnoyarsk:20240123T190000 +DTEND;TZID=Asia/Krasnoyarsk:20240123T193000 +SUMMARY:Без названия +UID:zwdqywax0szm0iv8br3yandex.ru +SEQUENCE:0 +DTSTAMP:20240126T105030Z +CREATED:20240123T110938Z +URL:https://calendar.yandex.ru/event?event_id=1980779831 +TRANSP:OPAQUE +CATEGORIES:Привет +LAST-MODIFIED:20240123T110938Z +CLASS:PRIVATE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Asia/Krasnoyarsk:20240123T223000 +DTEND;TZID=Asia/Krasnoyarsk:20240123T230000 +SUMMARY:Без названия +UID:zwdqywax0t03d7xqp32yandex.ru +SEQUENCE:0 +DTSTAMP:20240126T105030Z +CREATED:20240123T110941Z +URL:https://calendar.yandex.ru/event?event_id=1980779871 +TRANSP:OPAQUE +CATEGORIES:Привет +LAST-MODIFIED:20240123T110941Z +CLASS:PRIVATE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Samara:20240124T163000 +DTEND;TZID=Europe/Samara:20240124T170000 +SUMMARY:Привет +UID:zwdqywax08ke3e4lgn57yandex.ru +SEQUENCE:0 +DTSTAMP:20240126T105030Z +CREATED:20240124T090329Z +URL:https://calendar.yandex.ru/event?event_id=1981462656 +TRANSP:OPAQUE +CATEGORIES:Привет +LAST-MODIFIED:20240124T090329Z +CLASS:PRIVATE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Samara:20240126T130000 +DTEND;TZID=Europe/Samara:20240126T133000 +SUMMARY:Новое событие +UID:zwdqywax25jvjlx7r4e9yandex.ru +SEQUENCE:0 +DTSTAMP:20240126T105030Z +CREATED:20240126T080645Z +DESCRIPTION:Надо встретиться +URL:https://calendar.yandex.ru/event?event_id=1983061891 +TRANSP:OPAQUE +CATEGORIES:Привет +LAST-MODIFIED:20240126T080645Z +CLASS:PRIVATE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Samara:20240126T153000 +DTEND;TZID=Europe/Samara:20240126T160000 +SUMMARY:Ещё одно новое событие +UID:zwdqywax25jy3fg604q4yandex.ru +SEQUENCE:0 +DTSTAMP:20240126T105030Z +CREATED:20240126T080704Z +DESCRIPTION:Реально надо встретиться +URL:https://calendar.yandex.ru/event?event_id=1983062020 +TRANSP:OPAQUE +CATEGORIES:Привет +LAST-MODIFIED:20240126T080704Z +CLASS:PRIVATE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Samara:20240127T153000 +DTEND;TZID=Europe/Samara:20240127T160000 +SUMMARY:Без названия +UID:zwdqywax25sz6nhk0zfyandex.ru +SEQUENCE:0 +DTSTAMP:20240126T105030Z +CREATED:20240126T084915Z +URL:https://calendar.yandex.ru/event?event_id=1983093315 +TRANSP:OPAQUE +CATEGORIES:Привет +LAST-MODIFIED:20240126T084915Z +CLASS:PRIVATE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Samara:20240126T140000 +DTEND;TZID=Europe/Samara:20240126T143000 +SUMMARY:Тут ещё одно событие +UID:zwdqywax26494cx3kbv7yandex.ru +SEQUENCE:0 +DTSTAMP:20240126T105030Z +CREATED:20240126T094235Z +DESCRIPTION:Забей хуй на него +URL:https://calendar.yandex.ru/event?event_id=1983139091 +TRANSP:OPAQUE +CATEGORIES:Привет +LAST-MODIFIED:20240126T094235Z +CLASS:PRIVATE +END:VEVENT +END:VCALENDAR diff --git a/data/photo_edit_alarm.jpg b/data/photo_edit_alarm.jpg new file mode 100644 index 0000000..94305e2 Binary files /dev/null and b/data/photo_edit_alarm.jpg differ diff --git a/db/clock.db b/db/clock.db new file mode 100644 index 0000000..a7fda41 Binary files /dev/null and b/db/clock.db differ diff --git a/db/users.db b/db/users.db new file mode 100644 index 0000000..86b0d99 Binary files /dev/null and b/db/users.db differ