Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
TELEGRAM_TOKEN=
YANDEX_LOGIN=
YANDEX_PASSOWRD=
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,8 @@ dmypy.json

# Pyre type checker
.pyre/

test.json
test.py

*.db
46 changes: 36 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
# Получение уведомлений из системы Yandex Lyceum
Класс *YLNotifications(login, password)* предназначен для получения уведомлений из системы Yandex Lyceum. В качестве логина и пароля передаются данные для авторизации в Яндексе (и, соответственно, в LMS).
____
В коде описан пример отправки уведомлений в Telegram.
Также в коде описано несколько классов для возможной быстрой доработки в любой другой проект.
____
Классы были описаны для немногих вариантов уведомлений (для, наверное, самых важных). Если хотите больше - смело дорабатывайте!
____
Удачной учебы в Академии Яндекса!
____
@nvlastik

> переработка класса @nvlastik

## Quick start
Пишем боту @YLNotifierBot.

Чтобы зарегистрироваться пропишите данные от LMS в команду ```/enter [почта] [пароль]```

#### Описание команд

* ```/enable``` - включает отслеживание новых уведомлений

* ```/disable``` - отключает отслеживание новых уведомлений

* ```/all``` - выводит все непрочитанные уведомления

* ```/last``` - выводит последнее непрочитанное уведомление

* ```/register``` - получить указания по регистрации

* ```/enter [почта] [пароль]``` - ввод данных для регистрации

#### Примеры работы команд

![Пример команды last](https://sun9-5.userapi.com/impf/E7nk098X3AaFrrR-oCyqCxpUcGrr0xiCHF6YeA/5bpnHFASxGw.jpg?size=309x135&quality=96&sign=ce62d5751f2c457162f28b9a429a698a)

![Пример команды all](https://sun9-79.userapi.com/impf/8T6t17rIDXqqzQluxdfmYzS-bF5c65eW4qJB1g/aOlt2mY5_HI.jpg?size=514x312&quality=96&sign=5cedbd61b2f811b3d52104bb2f7c9c9a&type=album)

## Изменения
Класс был разделен на функционально независимые части. Добавлена возможность авторизации в Telegram с отслеживанием новых уведомлений.

## Возможности для доработки
куча...


[--> А это я <-- ](https://github.com/Yakser)
44 changes: 44 additions & 0 deletions _helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from models.notification import Notification
from models.user import User


def join_notifications(notifications):
return '\n\n'.join(notifications.values())


def get_notification_by_id(id, db_session):
session = db_session.create_session()
notification = session.query(Notification).get(id)
session.close()
return notification


def get_user_by_id(id, db_session):
session = db_session.create_session()
user = session.query(User).get(id)
session.close()
return user


def add_user(data, db_session):
try:
session = db_session.create_session()
user = User(id=data['id'], email=data['email'],
password=data['password'])
session.add(user)
session.commit()
session.close()
return user
except Exception as e:
print(e.__class__)
return False


def add_notifications_to_user(notification_ids, user_id, db_session):
session = db_session.create_session()
for notification_id in notification_ids:
notification = Notification(
id=int(notification_id), user_id=int(user_id))
session.add(notification)
session.commit()
session.close()
21 changes: 21 additions & 0 deletions _types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import TypedDict


class NotificationTypes:
NOTIFICATION_TASK_REVIEWED = 'task-solution-reviewed'
NOTIFICATION_TASK_COMMENTED = 'task-solution-commented'
NOTIFICATION_BONUS_SCORE_CHANGED = 'bonus-score-changed'
NOTIFICATION_LESSON_OPENED = 'lesson-opened'


class StatusTypes:
STATUS_REWORK = 'rework'
STATUS_ACCEPTED = 'accepted'


class NotificationDataType(TypedDict):
id: int
isRead: bool
type: str
addedTime: str
objectData: dict
188 changes: 188 additions & 0 deletions bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import logging
import os

from dotenv import load_dotenv
from telegram import Update
from telegram.ext import CallbackContext, CommandHandler, Updater

from _helpers import (add_notifications_to_user, add_user, get_user_by_id,
join_notifications)
from db import db_session
from main import YLNotifications

load_dotenv()

db_session.global_init("db/database.db")

CHECK_INTERVAL = 60 # in seconds

# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)

logger = logging.getLogger(__name__)


def check_notifications(context: CallbackContext):
job = context.job
notifier = job.context[2]
notifications = notifier.get_all_notifications()
user_id = job.context[1]

try:
user_notifications = set(get_user_by_id(
user_id, db_session).notifications)
user_notifications_id = set(
map(notif.id for notif in user_notifications))
notifications_to_add = set()

for notification_id, notification in notifications.items():
if notification_id not in user_notifications_id:
context.bot.send_message(
user_id, text=notification, parse_mode='HTML')
notifications_to_add.add(notification_id)

add_notifications_to_user(notifications_to_add, user_id, db_session)

except AttributeError:
context.bot.send_message(user_id,
text="<b>Вы не зарегистрированы!</b>", parse_mode='HTML')


def remove_job_if_exists(name: str, context: CallbackContext):
"""Remove job with given name. Returns whether job was removed."""
current_jobs = context.job_queue.get_jobs_by_name(name)
if not current_jobs:
return False
for job in current_jobs:
job.schedule_removal()
return True


def enable_notifications_checker(update: Update, context: CallbackContext):
"""Add a job to the queue."""

chat_id = update.message.chat_id
try:
job_removed = remove_job_if_exists(str(chat_id), context)
context.job_queue.run_repeating(
check_notifications, CHECK_INTERVAL, context=(chat_id, update.message.from_user['id'],
context.user_data['notifier']), name=str(chat_id))

text = 'Отслеживание уведомлений включено!'
if job_removed:
text = 'Уведомления уже отслеживаются!'
update.message.reply_text(text)
except KeyError:
update.message.reply_html(
'Сначала нужно зарегистрироваться!\n\n<code>/enter [почта] [пароль]</code>')
except (IndexError, ValueError):
update.message.reply_text('Использование: /enable')


def disable_notifications_checker(update: Update, context: CallbackContext):
"""Remove the job if the user changed their mind."""
chat_id = update.message.chat_id
job_removed = remove_job_if_exists(str(chat_id), context)
text = 'Отслеживание уведомлений отключено!' if job_removed else 'Отслеживание не включено.'
update.message.reply_text(text)


def start(update: Update, context: CallbackContext):
"""Send a message when the command /start is issued."""
user = update.effective_user
update.message.reply_markdown_v2(fr'Привет {user.mention_markdown_v2()}\! Я Yandex Lyceum Notifier Bot\.\
Буду отсылать тебе все уведомления из LMS :\)')


def help_command(update: Update, context: CallbackContext):
"""Send a message when the command /help is issued."""
update.message.reply_text(
'/enable - включить отслеживание уведомлений \n/disable - выключить\n/last - получить последнее уведомление\n/all - получить все уведомления\n')


def send_last_notification(update: Update, context: CallbackContext):
"""Send Yandex Lyceum notifications"""
try:
notifier = context.user_data['notifier']
update.message.reply_html(notifier.get_last_notification())
except KeyError:
update.message.reply_html(
'Сначала нужно зарегистрироваться!\n\n<code>/enter [почта] [пароль]</code>')


def send_all_notifications(update: Update, context: CallbackContext):
"""Send Yandex Lyceum notifications"""
try:
notifier = context.user_data['notifier']
update.message.reply_html(join_notifications(
notifier.get_all_notifications()))
except KeyError:
update.message.reply_html(
'Сначала нужно зарегистрироваться!\n\n<code>/enter [почта] [пароль]</code>')


def begin_register(update: Update, context: CallbackContext):
update.message.reply_html(
"<b>Через пробел введите команду <code>/enter</code> , почту, а затем пароль от LMS (кликните, чтобы скопировать шаблон):\n\n</b><code>/enter [почта] [пароль]</code>")


def finish_register(update: Update, context: CallbackContext):
email, password = context.args[0], context.args[1]
try:
notifier = YLNotifications(email, password)
context.user_data['notifier'] = notifier
if not add_user({'email': email, 'password': password, 'id': update.message.chat_id}, db_session):
update.message.reply_text(
'Вы уже зарегистрированы! Введите /help чтобы узнать подробнее обо всех командах')
else:
update.message.reply_text(
'Регистрация прошла успешно! Введите /help чтобы узнать подробнее обо всех командах')
except KeyError:
update.message.reply_html(
'Сначала нужно зарегистрироваться!\n\n<code>/enter [почта] [пароль]</code>')
except Exception as e:
print(e)
update.message.reply_text(
'Введены некорректные данные! Пожалуйста введите команду /enter повторно.')

# Удаление сообщения с данными пользователя
context.bot.delete_message(chat_id=update.message.chat_id,
message_id=update.message.message_id)


def main():
"""Start the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater(os.environ.get('TELEGRAM_TOKEN'))

# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

# on different commands - answer in Telegram
dispatcher.add_handler(CommandHandler("start", start))
dispatcher.add_handler(CommandHandler("help", help_command))

dispatcher.add_handler(CommandHandler("last", send_last_notification))
dispatcher.add_handler(CommandHandler("all", send_all_notifications))

dispatcher.add_handler(CommandHandler(
"enable", enable_notifications_checker))
dispatcher.add_handler(CommandHandler(
"disable", disable_notifications_checker))

dispatcher.add_handler(CommandHandler("register", begin_register))
dispatcher.add_handler(CommandHandler("enter", finish_register))

# Start the Bot
updater.start_polling()

# Run the bot until you press Ctrl-C or the process receives SIGINT,
# SIGTERM or SIGABRT. This should be used most of the time, since
# start_polling() is non-blocking and will stop the bot gracefully.
updater.idle()


if __name__ == '__main__':
main()
4 changes: 4 additions & 0 deletions db/__all_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import sqlalchemy
from .db_session import SqlAlchemyBase
from . import user
from . import notification
32 changes: 32 additions & 0 deletions db/db_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import sqlalchemy as sa
import sqlalchemy.ext.declarative as dec
import sqlalchemy.orm as orm
from sqlalchemy.orm import Session

SqlAlchemyBase = dec.declarative_base()

__factory = None


def global_init(db_file):
global __factory

if __factory:
return

if not db_file or not db_file.strip():
raise Exception("Необходимо указать файл базы данных.")

conn_str = f'sqlite:///{db_file.strip()}?check_same_thread=False'
print(f"Подключение к базе данных по адресу {conn_str}")

engine = sa.create_engine(conn_str, echo=False)
__factory = orm.sessionmaker(bind=engine)

SqlAlchemyBase.metadata.create_all(engine)


def create_session() -> Session:
"""Осуществляет получение сессии подключения к базе данных."""
global __factory
return __factory()
Loading