diff --git a/.gitignore b/.gitignore index fedd414..a93c9fe 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ SpreadsheetsService/utils/__pycache__ SpreadsheetsService/tests/__pycache__ SpreadsheetsService/manager/__pycache__ SpreadsheetsService/manager/client_secret.json -SpreadsheetsService/manager/token.json \ No newline at end of file +SpreadsheetsService/manager/token.json +TelegramBot/.secrets +SpreadsheetsService/.secrets +TelegramBot/messages/__pycache__ diff --git a/SpreadsheetsService/app.py b/SpreadsheetsService/app.py index 7a4e52b..5263a2d 100644 --- a/SpreadsheetsService/app.py +++ b/SpreadsheetsService/app.py @@ -1,9 +1,34 @@ -import flask -import time -import models +from functools import wraps from manager import * +import flask +import logging +import models +import time +LOG = logging.getLogger(__name__) app = flask.Flask(__name__) +app.db = None +try: + app.db = Manager() +except Exception as e: + app.db_error = e.args + LOG.exception("Cannot init manager") + + +def availability(func): + """Декоратор, который проверяет доступность менеджера по работе с Google + Spreadsheets. + + Если инициализация менеджера обвалилась, то сервер выдает ошибку 500 и ее + содержимое в JSON.""" + + @wraps(func) + def wrapper(*args, **kwargs): + if app.db is None: + return flask.jsonify(dict(error=True, message=app.db_error)), 500 + func(*args, **kwargs) + + return wrapper @app.route("/") @@ -12,13 +37,32 @@ def hello_world(): @app.route("/auth") +@availability def auth(): - # TODO: return json text instead of text/html. token = flask.request.args.get("token") tg_id = flask.request.args.get("tg_id") - manager = Manager() - student = manager.get_student_by_name("Дзюба") - return str(student) + student = app.db.get_student_by_name("Дзюба") + student = Student(1, "Фарид Михайлов", 5374) + return flask.jsonify(student) + + +@app.route("/grades") +@availability +def grades(): + """Возвращает оценки по id пользователя из телеграма.""" + tg_id = flask.request.args.get("tg_id") + student = models.Student(1, "Фарид Михайлов", 5374) + student.add_assignment(models.Assignment("ИДЗ 1", [1, 1, 0, 0, 1])) + student.add_assignment(models.Assignment("КР 1", [0.4, 0.8, 1, 0.5, 1], 5)) + student.add_assignment(models.Assignment("ИДЗ 2", [1, 1, 1, 0, 1])) + + return flask.jsonify( + dict( + score=student.score, + grade=student.grade, + assignments=student.assignments, + ) + ) if __name__ == "__main__": diff --git a/SpreadsheetsService/manager/manager.py b/SpreadsheetsService/manager/manager.py index abd374a..ada2d7c 100644 --- a/SpreadsheetsService/manager/manager.py +++ b/SpreadsheetsService/manager/manager.py @@ -20,8 +20,8 @@ ASSIGNMENT_SUBJECT = 3 ASSIGNMENT_WEIGHTS = 4 ASSIGNMENT_RANGES = 5 ASSIGNMENT_ALLOWS = 6 -TOKEN_FILE = pathlib.Path() / "manager" / "token.json" -CLIENT_SECRET_FILE = pathlib.Path() / "manager" / "client_secret.json" +TOKEN_FILE = pathlib.Path() / ".secrets" / "token.json" +CLIENT_SECRET_FILE = pathlib.Path() / ".secrets" / "client_secret.json" class Manager: diff --git a/SpreadsheetsService/models/assignment.py b/SpreadsheetsService/models/assignment.py index 7e85fa7..0d89206 100644 --- a/SpreadsheetsService/models/assignment.py +++ b/SpreadsheetsService/models/assignment.py @@ -1,10 +1,17 @@ -class Assignment: +class Assignment(dict): """Задание.""" - def __init__(self, name: str, values: list, weight: float=1): + def __init__(self, name: str, points: list, weight: float = 1): self.name = name self.weight = weight - self.values = values + self.points = points + dict.__init__( + self, + name=self.name, + weight=self.weight, + points=self.points, + total=self.total, + ) @property def total(self) -> float: @@ -12,4 +19,4 @@ class Assignment: `вес*СУММ(рез-ты)` """ - return sum(self.values) * self.weight + return sum(self.points) * self.weight diff --git a/SpreadsheetsService/models/student.py b/SpreadsheetsService/models/student.py index 6040665..9d39df5 100644 --- a/SpreadsheetsService/models/student.py +++ b/SpreadsheetsService/models/student.py @@ -1,8 +1,7 @@ from .assignment import Assignment -import json -class Student: +class Student(dict): """Позволяет выполнять операции над студентами.""" def __init__(self, number: int, name: str, group_id: int, tg_id: int = None): @@ -12,6 +11,15 @@ class Student: self.tg_id: int = tg_id self.assignments = [] + dict.__init__( + self, + number=self.number, + name=self.name, + group_id=self.group_id, + tg_id=self.tg_id, + assignments=self.assignments, + ) + def add_assignment(self, assignment: Assignment): self.assignments.append(assignment) @@ -24,13 +32,3 @@ class Student: def score(self): """Количество баллов у студента.""" return sum([a.total for a in self.assignments]) - - def __str__(self): - return json.dumps( - { - "number": self.number, - "name": self.name, - "group_id": self.group_id, - "tg_id": self.tg_id, - } - ) diff --git a/SpreadsheetsService/requirements.txt b/SpreadsheetsService/requirements.txt index f0b48a0..0e22ebc 100644 --- a/SpreadsheetsService/requirements.txt +++ b/SpreadsheetsService/requirements.txt @@ -1 +1,3 @@ -Flask==2.0.0 \ No newline at end of file +Flask==2.0.0 +google-api-python-client==2.15.0 +google-auth-oauthlib==0.4.5 \ No newline at end of file diff --git a/TelegramBot/Dockerfile b/TelegramBot/Dockerfile index f5e5e93..385874e 100644 --- a/TelegramBot/Dockerfile +++ b/TelegramBot/Dockerfile @@ -12,7 +12,6 @@ COPY requirements.txt . RUN python -m pip install -r requirements.txt WORKDIR /app -COPY . /app # Creates a non-root user with an explicit UID and adds permission to access the /app folder # For more info, please refer to https://aka.ms/vscode-docker-python-configure-containers diff --git a/TelegramBot/bot.py b/TelegramBot/bot.py index 3254a81..de2967f 100644 --- a/TelegramBot/bot.py +++ b/TelegramBot/bot.py @@ -1,58 +1,97 @@ import logging -import os -import platform -import time import requests +from pathlib import Path -from telegram.ext import Updater -from telegram.ext import CommandHandler -from telegram.ext import MessageHandler, Filters -TOKEN = "1903048555:AAFK-d7prKvOvwwXo0IPzeRwJ1ZcuOT5ty8" # os.environ["TOKEN"] +from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update +from telegram.ext import ( + Updater, + CommandHandler, + MessageHandler, + Filters, + ConversationHandler, + CallbackContext, +) + +from messages import Messages + +TOKEN = Path(".secrets/bot_token.txt").read_text() +MESSAGES = Messages() +HOST = "http://carolyn-spreadsheets:5000" logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) +LOG = logging.getLogger(__name__) +LOG.debug("Token is %s", TOKEN) updater = Updater(token=TOKEN, use_context=True) dispatcher = updater.dispatcher # ================================================================ BOT COMMANDS -# Стартовое сообщение (к команде /start) -def start(update, context): + +def start(update: Update, context: CallbackContext): + """Стартовое сообщение (к команде /start)""" + + user_id = update.message.from_user.id p = context.args[0] if len(context.args) > 0 else None if p is None: - context.bot.send_message( - chat_id=update.effective_chat.id, - text="Нет доступа к боту", - ) + update.message.reply_markdown_v2(text=MESSAGES.auth.NO_AUTH) return - data = requests.get( - "http://127.0.0.1:5000/auth", - params={"token": p, "tg_id": update.effective_chat.id}, - ).json() + update.message.reply_text(text=MESSAGES.auth.START) + + LOG.info(f"Authenticanting {user_id}") + + try: + data = requests.get( + f"{HOST}/auth", + params={"token": p, "tg_id": user_id}, + ).json() + except requests.exceptions.ConnectionError: + update.message.reply_sticker(MESSAGES.stickers.DEAD) + return context.bot.send_message( - chat_id=update.effective_chat.id, - text=f"Привет, {data}", + text=MESSAGES.auth.hello(data), ) -# Ответ на не командное сообщение (отвечает тем же сообщением) -def echo(update, context): +def grades(update: Update, context: CallbackContext): + """Команда: Получить оценки студента.""" + + user_id = update.message.from_user.id + try: + data = requests.get(f"{HOST}/grades", params={"tg_id": user_id}).json() + score = data["score"] + grade = data["grade"] + n_of_assignments = len(data["assignments"]) + + score_message = MESSAGES.score.get(grade, score, n_of_assignments) + update.message.reply_html(score_message) + update.message.reply_sticker(MESSAGES.stickers.bad()) + except: + LOG.exception("Failed to get grades.") + update.message.reply_sticker(MESSAGES.stickers.DEAD) + + +def echo(update: Update, context: CallbackContext): + """Ответ на не командное сообщение (отвечает тем же сообщением)""" context.bot.send_message(chat_id=update.effective_chat.id, text=update.message.text) # ==================================================================== HANDLERS start_handler = CommandHandler("start", start) +grades_handler = CommandHandler("grades", grades) + echo_handler = MessageHandler(Filters.text & (~Filters.command), echo) dispatcher.add_handler(start_handler) +dispatcher.add_handler(grades_handler) dispatcher.add_handler(echo_handler) # Начало работы бота diff --git a/TelegramBot/messages/__init__.py b/TelegramBot/messages/__init__.py new file mode 100644 index 0000000..da79b07 --- /dev/null +++ b/TelegramBot/messages/__init__.py @@ -0,0 +1 @@ +from .messages import Messages diff --git a/TelegramBot/messages/messages.py b/TelegramBot/messages/messages.py new file mode 100644 index 0000000..4eff5ba --- /dev/null +++ b/TelegramBot/messages/messages.py @@ -0,0 +1,84 @@ +import random + + +class Messages: + class Auth: + def __init__(self): + self.NO_AUTH = "[Правила авторизации](https://github.com/Faridik/Carolyn/wiki/%F0%9F%A6%B8%E2%80%8D%E2%99%82%EF%B8%8F-%D0%90%D0%B2%D1%82%D0%BE%D1%80%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F)" + self.START = "🔑 Авторизация..." + self.NO_ACCESS = "⛔ Нет доступа" + + def hello(self, data): + return f"Привет, {data} 👋" + + class Score: + def get(self, grade, score, n_of_assignments): + summary = ( + f"Ранг: {score:.2f} {n_of_assignments} зад.\n" + + f"Оценка: {grade:.2f}" + ) + return summary + + class Spreadsheets: + def __init__(self): + self.DEAD = "⚰ Сервис «таблицы» недоступен" + + class Stickers: + def __init__(self): + # Воробей павлик с колокольчиком и надписью "позор" + self.POZOR = "CAACAgIAAxkBAAEm1RhhBZg-2D6NypUeTsnbToD8_LMFTAACuUcAAulVBRjfnofQTkeMnCAE" + # кот Сима в петле плачет и показывает палец вверх + self.ROPE = "CAACAgIAAxkBAAEm1SFhBZjLaZgFIkvyxfBmAplwUWQFwwACBS0AAulVBRiIaSQ42WLoyyAE" + # Акио смотрит на брокколи + self.BROCCOLI = "CAACAgIAAxkBAAEm1SVhBZkd24Dy2B_wYSCi6UrNuUjgwQACEUkAAulVBRiykDzrzywbmiAE" + # Ян показывает палец вниз + self.THUMB_DOWN = "CAACAgIAAxkBAAEm1SthBZlcos0_TbJo8W7mAcqGLNAh2wACJ5cAAp7OCwABDddMclD7Tf8gBA" + # Тигр закатывает глаза + self.EYES_UP = "CAACAgEAAxkBAAEm1TBhBZmIZTrwzhGSzC47rNFbewQe6QAC4gADOA6CEeTgk4YIcTeWIAQ" + # Нутри большими глазами и надписью "пипец" + self.PPTS = "CAACAgIAAxkBAAEm1TZhBZnNVwLXmrbZwvdGjxiSwefzDAACn0sAAulVBRhECmBPSGCXHSAE" + # Попугай Кент с недовольной мордахой + self.GRUMPY_KENT = "CAACAgIAAxkBAAEm1UBhBZn1gXmRfxaK_RqFTj1aFxLY-gACy0EAAulVBRhgQdlH2rd0uyAE" + # wojak + self.WOJAK = "CAACAgIAAxkBAAEm1UJhBZpfHNYVT_ZLrxUpyi8qRLWJGgACPAADB7lDCi2YrzAEyxJeIAQ" + # Лэтишный котик и надпись "ОЙ" + self.OOPS = "CAACAgIAAxkBAAEm1URhBZp2li7w2E1M6PirG7UnqLOc2QACSxEAAlTE4UoR1Olz8RANZCAE" + # Мем "оп ахах неловко вышло" + self.NELOVKO = "CAACAgIAAxkBAAEm1UZhBZqZ94SFa1e6d-Zy5I-6_SC_fQACgwgAAh8MqEiLbHl9nugoYSAE" + # Попугай смотрит в зеркало "посмотри что с тобой стало" + self.MIRROR = "CAACAgIAAxkBAAEm1VFhBZudma_FmFhybGgXikz3csc0BQAC2UEAAulVBRgkdje995ir7iAE" + # Попугай с сигаретой + self.CIG = "CAACAgIAAxkBAAEm1VZhBZv6p7GsHS6wcyt2mCXoHJCo-wAC0UEAAulVBRghLLSQAcU6rSAE" + # Попугай и песок + self.SAND = "CAACAgIAAxkBAAEm1VhhBZwbw83_UVS8A5Crsf3EVAABvWIAAshBAALpVQUYl51K7ci-ClIgBA" + # Акио падает в обморок + self.PANIC = "CAACAgIAAxkBAAEm1VxhBZxwqRUJmVXDtzmsPJUIIGJgegACB0kAAulVBRiMI-hzK3GcKiAE" + # Эникей надпись "умер" + self.DEAD = "CAACAgIAAxkBAAEm1aRhBaWXRb_tcI03O1onx0jW0bHmuAACejcAAulVBRi1oSUXeBATmiAE" + + def bad(self): + """Возвращает стикер с грустным смыслом.""" + return random.choice( + [ + self.POZOR, + self.ROPE, + self.BROCCOLI, + self.THUMB_DOWN, + self.EYES_UP, + self.PPTS, + self.GRUMPY_KENT, + self.WOJAK, + self.OOPS, + self.NELOVKO, + self.MIRROR, + self.CIG, + self.SAND, + self.PANIC, + ] + ) + + def __init__(self): + self.auth = self.Auth() + self.score = self.Score() + self.spreadsheets = self.Spreadsheets() + self.stickers = self.Stickers() diff --git a/docker-compose.yml b/docker-compose.yml index 4b234a2..5ebf360 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,17 @@ version: "3.9" -services: - spreadsheets: - container_name: "carolyn-spreadsheets" - build: ./SpreadsheetsService - ports: - - "5000:5000" - environment: - SECRETKEY: "HELLO-WORLD-@21XX 15AF" - volumes: - - ./SpreadsheetsService:/app - telegrambot: - container_name: "carolyn-telegrambot" - build: ./TelegramBot - environment: - TOKEN: "1903048555:AAFK-d7prKvOvwwXo0IPzeRwJ1ZcuOT5ty8" - volumes: - - ./TelegramBot:/app \ No newline at end of file +services: + spreadsheets: + container_name: "carolyn-spreadsheets" + build: ./SpreadsheetsService + ports: + - "5000:5000" + environment: + SECRETKEY: "HELLO-WORLD-@21XX 15AF" + volumes: + - ./SpreadsheetsService:/app + telegrambot: + container_name: "carolyn-telegrambot" + build: ./TelegramBot + volumes: + - ./TelegramBot:/app