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