Привет! Раньше мы с тобой работали над кодом и теперь я хочу его усовершенствовать! Код скину после того, что хочу туда добавить. Попрошу не удалять ничего лишнего. После того как закончишь вышли полный готовый код. Вот что меня интересует:
1. Из соображений безопасности хранить токен бота в переменных окружения или в отдельном конфигурационном файле, а не напрямую в коде
2. Усилить проверку вводимых пользователями данных для предотвращения SQL-инъекций и других видов атак
3. Предоставить администраторам отчёты о количестве пользователей, активности и других метрик

Как я хочу, чтобы выглядела реализация каждого из пунктов попунктно:
1. Сделай команду в начале кода, который создаст такой файл. Я потом удалю эту строку кода. И код для токена в этот файл в основном коде тоже адаптируй, чтоб всё как надо работало. 
2. Я не знаю как это сделать, но попытайся сам это сделать, может быть, что-то действенное придумаешь. Хочу прям максимально действенную защиту, чтоб никто не обошёл!
3. Здесь стоит поднапрячься. В личном кабинете все функции, которые открывались администраторам, добавь в отдельную кнопку "Для администраторов". Там рассылки, в основном. Там в самом сверху, над кнопками с расслыками, добавь кнопку "Статистика". В ней пусть будет входить какая-нибудь базовая информация. По типу количество пользователей всего, новых за сегодня, новых за вчера, пользовавшихся ботом сегодня, пользовавшихся ботом вчера, самая активная группа по пользованию бота (в сообщениях) и прочее.

Сам код:
import pytz
import sqlite3
from datetime import datetime, timedelta
from telegram import Update, ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes, MessageHandler, filters, CallbackQueryHandler, CallbackContext

def init_db():
    conn = sqlite3.connect('university.db')
    c = conn.cursor()

    c.execute('''CREATE TABLE IF NOT EXISTS schedule
                 (course INTEGER, direction TEXT, week_type TEXT, day TEXT, slot INTEGER, time TEXT, details TEXT)''')

    c.execute('''CREATE TABLE IF NOT EXISTS users
                 (id INTEGER PRIMARY KEY, username TEXT, course INTEGER, direction TEXT, subscribed INTEGER DEFAULT 1)''')

    c.execute('''CREATE TABLE IF NOT EXISTS admins
                 (id INTEGER PRIMARY KEY)''')

    c.execute("DELETE FROM schedule")

    times = ["8:30 - 10:00", "10:10 - 11:40", "12:30 - 14:00", "14:10 - 15:40", "15:50 - 17:20", "17:30 - 19:00"]
    directions = ['Ф', 'У', 'ФМ', 'ГМУ', 'БИ']

    schedule_bi = {
        "чётная": {
            "Понедельник": ["Матем (л) (222)", "Матем (с) (222)", "Физ-ра", "Англ (209)", "", ""],
            "Вторник": ["История (с) (108)", "История (л) (108)", "ИТПД (с) (217)", "", "", ""],
            "Среда": ["", "", "", "ИТПД (с) (217)", "Phython (л) (111)", "Phython (л) (111)"],
            "Четверг": ["", "История (с) (204)", "Менеджмент (л) (209)", "", "", ""],
            "Пятница": ["", "", "", "БЖД (л) (304)", "Менеджмент (с) (108)", "Физ-ра"]
        },
        "нечётная": {
            "Понедельник": ["Матем (л) (222)", "Матем (с) (222)", "Физ-ра", "Англ (209)", "", ""],
            "Вторник": ["Англ (209)", "История (л) (108)", "ИТПД (с) (217)", "История ФУ (л) (211)", "", ""],
            "Среда": ["", "", "", "ИТПД (с) (217)", "Введение в спец (л) (111)", "Phython (с) (111)"],
            "Четверг": ["Введение в спец (с) (213)", "Матем (с) (204)", "История (с) (209)", "", "", ""],
            "Пятница": ["", "", "", "БЖД (с) (304)", "Менеджмент (с) (108)", "Физ-ра"]
        }
    }

    for course in range(1, 5):
        for direction in directions:
            schedule_data = {
                "чётная": {day: [""] * 6 for day in ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"]},
                "нечётная": {day: [""] * 6 for day in ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"]}
            }

            if direction == 'БИ' and course == 1:
                schedule_data = schedule_bi
            else:
                for week_type, days in schedule_data.items():
                    for day in days:
                        days[day][0] = f"Курс {course}, Группа {direction}"

            for week_type, days in schedule_data.items():
                for day, slots in days.items():
                    for slot in range(6):
                        time = times[slot]
                        detail = slots[slot]
                        c.execute("INSERT INTO schedule (course, direction, week_type, day, slot, time, details) VALUES (?, ?, ?, ?, ?, ?, ?)",
                                  (course, direction, week_type, day, slot + 1, time, detail))

    conn.commit()
    conn.close()

def get_day_name(index):
    days_rus = [
        "Понедельник",
        "Вторник",
        "Среда",
        "Четверг",
        "Пятница",
        "Суббота",
        "Воскресенье"
    ]
    return days_rus[index]

def is_even_week(date):
    week_number = date.isocalendar()[1]
    return week_number % 2 == 0

def get_week_type(date):
    return 'чётная' if is_even_week(date) else 'нечётная'

def get_week_type_short(date):
    return 'чёт' if is_even_week(date) else 'нечёт'

def get_schedule_for_day(course, direction, day_name, date):
    week_type = get_week_type(date)
    conn = sqlite3.connect('university.db')
    c = conn.cursor()

    c.execute(
        "SELECT slot, time, details FROM schedule WHERE course = ? AND direction = ? AND week_type = ? AND day = ? ORDER BY slot",
        (course, direction, week_type, day_name)
    )
    results = c.fetchall()
    conn.close()

    if not results:
        return "Нет занятий"

    day_schedule = ""
    for slot, time, details in results:
        if details:
            day_schedule += f"{slot}. {time}\n{details}\n\n"

    return day_schedule.strip() if day_schedule else "Нет занятий"

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    await update.message.reply_text('Привет! Поменяй группу и курс на соответствующие в своём личном кабинете.')
    await personal_cabinet(update, context)

    keyboard = [
        ["Выбрать дату расписания", "Расписание на завтра", "Расписание на сегодня"],
        ["Понедельник", "Вторник", "Среда"],
        ["Четверг", "Пятница", "Суббота"],
        ["Личный кабинет"]
    ]

    reply_markup = ReplyKeyboardMarkup(keyboard, one_time_keyboard=True, resize_keyboard=True)

    await update.message.reply_text('Выбери опцию:', reply_markup=reply_markup)

async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user_message = update.message.text

    if context.user_data.get('awaiting_date', False):
        if user_message.isdigit():
            day = int(user_message)
            current_month = datetime.now(pytz.timezone('Asia/Omsk')).month
            current_year = datetime.now(pytz.timezone('Asia/Omsk')).year
            try:
                date_obj = datetime(current_year, current_month, day, tzinfo=pytz.timezone('Asia/Omsk'))
                if date_obj.month != current_month:
                    raise ValueError
                await show_schedule_by_date(update, context, date_obj)
            except ValueError:
                await update.message.reply_text('Неверный день. Пожалуйста, введи число от 1 до последнего дня месяца.')
            finally:
                context.user_data['awaiting_date'] = False
        else:
            await update.message.reply_text('Пожалуйста, введи число для получения расписания.')
        return

    if context.user_data.get('is_sending_announcement', False):
        await collect_announcement(update, context)
        return

    if user_message.lower() in ["расписание на сегодня", "расписание на завтра", "понедельник", "вторник", "среда", "четверг", "пятница", "суббота"]:
        await show_schedule(update, context, user_message)

    elif user_message.lower() == "выбрать дату расписания":
        context.user_data['awaiting_date'] = True
        await update.message.reply_text("Введи необходимое число этого месяца:")

    elif user_message.lower() == "личный кабинет":
        await personal_cabinet(update, context)

    else:
        await update.message.reply_text('Выбери опцию из меню.')

async def personal_cabinet(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    username = update.effective_user.username
    first_name = update.effective_user.first_name
    last_name = update.effective_user.last_name or ""

    conn = sqlite3.connect('university.db')
    c = conn.cursor()
    c.execute("SELECT username, course, direction, subscribed FROM users WHERE id = ?", (user_id,))
    user = c.fetchone()

    if not user:
        c.execute("INSERT INTO users (id, username, course, direction) VALUES (?, ?, ?, ?)", (user_id, username, 1, "Ф"))
        conn.commit()
        user = (username, 1, "Ф", 1)

    c.execute("SELECT 1 FROM admins WHERE id = ?", (user_id,))
    is_admin = c.fetchone() is not None

    conn.close()

    current_date = datetime.now(pytz.timezone('Asia/Omsk'))
    week_number = current_date.isocalendar()[1]
    week_type = get_week_type(current_date)
    week_status = f"{week_number} {week_type}"

    role = "Преподаватель" if is_admin else "Студент"
    subscribe_state = "Отписаться от уведомлений университета" if user[3] else "Подписаться на уведомления университета"

    message_text = (
        f"Личный кабинет\n\n"
        f"Имя: {first_name} {last_name}\n"
        f"Ник: @{user[0]}\n"
        f"ID: {user_id}\n\n"
        f"Роль: {role}\n"
        f"Группа: {user[2]}\n"
        f"Курс: {user[1]}\n\n"
        f"Неделя: {week_status}\n"
        f"Дата: {current_date.strftime('%d.%m.%Y')}\n"
        f"Время: {current_date.strftime('%H:%M:%S')}"
    )

    keyboard = [
        [
            InlineKeyboardButton("Изменить группу", callback_data='change_direction'),
            InlineKeyboardButton("Изменить курс", callback_data='change_course')
        ],
        [InlineKeyboardButton(subscribe_state, callback_data='toggle_subscription')]
    ]

    if is_admin:
        keyboard.append([
            InlineKeyboardButton("Сделать рассылку", callback_data='send_announcement'),
            InlineKeyboardButton("Глобальная рассылка", callback_data='send_global_announcement')
        ])

    reply_markup = InlineKeyboardMarkup(keyboard)

    if update.callback_query:
        await update.callback_query.edit_message_text(text=message_text, reply_markup=reply_markup)
    else:
        await update.message.reply_text(text=message_text, reply_markup=reply_markup)

async def handle_callback(update: Update, context: CallbackContext):
    query = update.callback_query
    user_id = query.from_user.id
    conn = sqlite3.connect('university.db')
    c = conn.cursor()

    if query.data == 'change_course':
        c.execute("SELECT course FROM users WHERE id = ?", (user_id,))
        current_course = c.fetchone()[0]
        new_course = 1 if current_course == 4 else current_course + 1
        c.execute("UPDATE users SET course = ? WHERE id = ?", (new_course, user_id))
        conn.commit()
        await query.answer(f"Курс изменён на {new_course}")
        await personal_cabinet(update, context)

    elif query.data == 'change_direction':
        c.execute("SELECT direction FROM users WHERE id = ?", (user_id,))
        current_direction = c.fetchone()[0]
        directions = ['Ф', 'У', 'ФМ', 'ГМУ', 'БИ']
        new_direction_index = (directions.index(current_direction) + 1) % len(directions)
        new_direction = directions[new_direction_index]
        c.execute("UPDATE users SET direction = ? WHERE id = ?", (new_direction, user_id))
        conn.commit()
        await query.answer(f"Группа изменена на {new_direction}")
        await personal_cabinet(update, context)

    elif query.data == 'toggle_subscription':
        c.execute("SELECT subscribed FROM users WHERE id = ?", (user_id,))
        current_subscription = c.fetchone()[0]
        new_subscription = 0 if current_subscription else 1
        c.execute("UPDATE users SET subscribed = ? WHERE id = ?", (new_subscription, user_id))
        conn.commit()
        await query.answer("Подписка обновлена")
        await personal_cabinet(update, context)

    elif query.data == 'send_announcement' and is_admin_user(user_id):
        c.execute("SELECT course, direction FROM users WHERE id = ?", (user_id,))
        admin_info = c.fetchone()
        context.user_data['announcement_for'] = admin_info[:2]  # course, direction
        context.user_data['is_sending_announcement'] = True
        await query.answer("Введите сообщение для рассылки вашей группе")

    elif query.data == 'send_global_announcement' and is_admin_user(user_id):
        context.user_data['announcement_for'] = 'global'
        context.user_data['is_sending_announcement'] = True
        await query.answer("Введите сообщение для глобальной рассылки")

    elif query.data == 'confirm_send':
        await send_announcement(context)
        await update.callback_query.edit_message_text("Успешная отправка.")
        context.user_data['announcement_for'] = None
        await query.answer("Рассылка отправлена.")

    elif query.data == 'cancel_send':
        await update.callback_query.edit_message_text("Отправка отменена.")
        context.user_data['announcement_for'] = None
        await query.answer("Рассылка отменена.")

    conn.close()

async def collect_announcement(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not context.user_data.get('is_sending_announcement', False):
        return

    message_text = update.message.text
    if not message_text:
        return

    context.user_data['message_text'] = message_text
    context.user_data['is_sending_announcement'] = False

    confirm_message = f"Вот ваш текст:\n\n{message_text}\n\nХотите отправить?"
    buttons = [
        [
            InlineKeyboardButton("Да", callback_data='confirm_send'),
            InlineKeyboardButton("Нет", callback_data='cancel_send')
        ]
    ]
    reply_markup = InlineKeyboardMarkup(buttons)

    await update.message.reply_text(confirm_message, reply_markup=reply_markup)

async def send_announcement(context: ContextTypes.DEFAULT_TYPE):
    announcement_for = context.user_data.get('announcement_for')
    message_text = context.user_data.get('message_text')

    if not announcement_for or not message_text:
        return

    context.user_data['announcement_for'] = None
    context.user_data['message_text'] = None

    conn = sqlite3.connect('university.db')
    c = conn.cursor()

    if announcement_for == 'global':
        c.execute("SELECT id FROM users WHERE subscribed = 1")
        subscribers = c.fetchall()
        c.execute("SELECT id FROM admins")
        admins = c.fetchall()
    else:
        course, direction = announcement_for
        c.execute("SELECT id FROM users WHERE course = ? AND direction = ? AND subscribed = 1", (course, direction))
        subscribers = c.fetchall()
        c.execute("SELECT id FROM admins")
        admins = c.fetchall()

    conn.close()

    all_recipients = set(subscribers) | set(admins)

    for recipient in all_recipients:
        try:
            await context.bot.send_message(chat_id=recipient[0], text=message_text)
        except Exception as e:
            print(f"Failed to send message to user {recipient[0]}: {e}")

def is_admin_user(user_id):
    conn = sqlite3.connect('university.db')
    c = conn.cursor()
    c.execute("SELECT 1 FROM admins WHERE id = ?", (user_id,))
    is_admin = c.fetchone() is not None
    conn.close()
    return is_admin

async def show_schedule(update: Update, context: ContextTypes.DEFAULT_TYPE, day_command: str):
    user_id = update.message.from_user.id

    conn = sqlite3.connect('university.db')
    c = conn.cursor()
    c.execute("SELECT course, direction FROM users WHERE id = ?", (user_id,))
    user_info = c.fetchone()

    if not user_info:
        await personal_cabinet(update, context)
        c.execute("SELECT course, direction FROM users WHERE id = ?", (user_id,))
        user_info = c.fetchone()

        if not user_info:
            await update.message.reply_text("Ошибка при добавлении пользователя в базу данных.")
            return

    course, direction = user_info
    conn.close()

    current_date = datetime.now(pytz.timezone('Asia/Omsk'))
    current_weekday = current_date.weekday()

    days_map = {
        "расписание на сегодня": 0,
        "расписание на завтра": 1,
        "понедельник": 0,
        "вторник": 1,
        "среда": 2,
        "четверг": 3,
        "пятница": 4,
        "суббота": 5
    }
    day_index = days_map[day_command.lower()]

    if day_command.lower() == "расписание на сегодня":
        target_date = current_date
    elif day_command.lower() == "расписание на завтра":
        target_date = current_date + timedelta(days=1)
    else:
        day_offset = (day_index - current_weekday) % 7
        target_date = current_date + timedelta(days=day_offset)

    day_name = get_day_name(target_date.weekday())
    week_type_short = get_week_type_short(target_date)
    day_schedule = get_schedule_for_day(course, direction, day_name, target_date)

    message_text = f"{day_name} {target_date.strftime('%d.%m.%Y')} ({week_type_short}):\n\n{day_schedule}"
    await update.message.reply_text(message_text)

async def show_schedule_by_date(update: Update, context: ContextTypes.DEFAULT_TYPE, date_obj):
    user_id = update.message.from_user.id

    conn = sqlite3.connect('university.db')
    c = conn.cursor()
    c.execute("SELECT course, direction FROM users WHERE id = ?", (user_id,))
    user_info = c.fetchone()

    if not user_info:
        await personal_cabinet(update, context)
        c.execute("SELECT course, direction FROM users WHERE id = ?", (user_id,))
        user_info = c.fetchone()

        if not user_info:
            await update.message.reply_text("Ошибка при добавлении пользователя в базу данных.")
            return

    course, direction = user_info
    conn.close()

    day_name = get_day_name(date_obj.weekday())
    week_type_short = get_week_type_short(date_obj)
    day_schedule = get_schedule_for_day(course, direction, day_name, date_obj)

    message_text = f"{day_name} {date_obj.strftime('%d.%m.%Y')} ({week_type_short}):\n\n{day_schedule}"
    await update.message.reply_text(message_text)

if __name__ == '__main__':
    init_db()
    application = ApplicationBuilder().token("<TRUFFLEHOG_REDACTED_TELEGRAMBOTTOKEN>").build()

    application.add_handler(CommandHandler("start", start))
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
    application.add_handler(CallbackQueryHandler(handle_callback))

    application.run_polling()