From 12cc75c4cb22582646f58b04ec033f2275e2477a Mon Sep 17 00:00:00 2001 From: leddest <46251307+leddest@users.noreply.github.com> Date: Wed, 20 Jan 2021 19:00:48 +0200 Subject: [PATCH 01/16] Minor changes --- app/main.py | 2 +- app/routers/profile.py | 11 ----------- tests/test_profile.py | 1 + 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/app/main.py b/app/main.py index 3af7bee2..b48bdae5 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from app.database.database import engine from app.dependencies import ( MEDIA_PATH, STATIC_PATH, templates) -from .routers import agenda, event, profile +from app.routers import agenda, event, profile models.Base.metadata.create_all(bind=engine) diff --git a/app/routers/profile.py b/app/routers/profile.py index 39724939..3d73a481 100644 --- a/app/routers/profile.py +++ b/app/routers/profile.py @@ -44,8 +44,6 @@ async def profile( session.commit() user = session.query(User).filter_by(id=1).first() - session.close() - return templates.TemplateResponse("profile.html", { "request": request, "user": user, @@ -65,8 +63,6 @@ async def update_user_fullname( user.full_name = new_fullname session.commit() - session.close() - url = router.url_path_for("profile") response = RedirectResponse(url=url, status_code=HTTP_302_FOUND) return response @@ -84,8 +80,6 @@ async def update_user_email( user.email = new_email session.commit() - session.close() - url = router.url_path_for("profile") return RedirectResponse(url=url, status_code=HTTP_302_FOUND) @@ -102,8 +96,6 @@ async def update_profile( user.description = new_description session.commit() - session.close() - url = router.url_path_for("profile") return RedirectResponse(url=url, status_code=HTTP_302_FOUND) @@ -119,10 +111,7 @@ async def upload_user_photo( # Save to database user.avatar = await process_image(pic, user) session.commit() - finally: - session.close() - url = router.url_path_for("profile") return RedirectResponse(url=url, status_code=HTTP_302_FOUND) diff --git a/tests/test_profile.py b/tests/test_profile.py index 4fbb33b9..4385bc1a 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -107,3 +107,4 @@ def test_upload_user_photo(profile_test_client): # Validate new picture size new_avatar_path = os.path.join(MEDIA_PATH, 'fake_user.png') assert Image.open(new_avatar_path).size == config.AVATAR_SIZE + os.remove(new_avatar_path) From a7793c355a0bc8202eb0dff3654fe57dd9d1f99d Mon Sep 17 00:00:00 2001 From: leddest <46251307+leddest@users.noreply.github.com> Date: Thu, 21 Jan 2021 18:00:39 +0200 Subject: [PATCH 02/16] Add telegram client --- app/config.py.example | 4 ++ app/database/models.py | 1 + app/main.py | 8 ++- app/routers/profile.py | 19 +++++- app/routers/telegram.py | 41 ++++++++++++ app/telegram/__init__.py | 0 app/telegram/handlers.py | 55 ++++++++++++++++ app/telegram/keyboards.py | 0 app/telegram/models.py | 40 +++++++++++ app/telegram/pylander.py | 16 +++++ app/templates/profile.html | 50 ++++++++++++-- requirements.txt | 2 + tests/conftest.py | 3 +- tests/test_profile.py | 17 +++++ tests/test_telegram.py | 131 +++++++++++++++++++++++++++++++++++++ 15 files changed, 376 insertions(+), 11 deletions(-) create mode 100644 app/routers/telegram.py create mode 100644 app/telegram/__init__.py create mode 100644 app/telegram/handlers.py create mode 100644 app/telegram/keyboards.py create mode 100644 app/telegram/models.py create mode 100644 app/telegram/pylander.py create mode 100644 tests/test_telegram.py diff --git a/app/config.py.example b/app/config.py.example index b4c8ccf2..21a5dfc6 100644 --- a/app/config.py.example +++ b/app/config.py.example @@ -8,3 +8,7 @@ DEVELOPMENT_DATABASE_STRING = "sqlite:///./dev.db" MEDIA_DIRECTORY = 'media' PICTURE_EXTENSION = '.png' AVATAR_SIZE = (120, 120) + +# TELEGRAM +BOT_API = "paste bot-api" +WEBHOOK_URL = "use real webhook url or generate webhook for your local host with Ngrok" diff --git a/app/database/models.py b/app/database/models.py index 0c92ae94..310dd288 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -14,6 +14,7 @@ class User(Base): full_name = Column(String) description = Column(String, default="Happy new user!") avatar = Column(String, default="profile.png") + telegram_id = Column(String, unique=True) is_active = Column(Boolean, default=True) diff --git a/app/main.py b/app/main.py index b48bdae5..545216c6 100644 --- a/app/main.py +++ b/app/main.py @@ -1,11 +1,14 @@ from fastapi import FastAPI, Request +from starlette.responses import RedirectResponse +from starlette.status import HTTP_302_FOUND from fastapi.staticfiles import StaticFiles from app.database import models from app.database.database import engine from app.dependencies import ( MEDIA_PATH, STATIC_PATH, templates) -from app.routers import agenda, event, profile +from app.telegram.pylander import pylander +from app.routers import agenda, event, profile, telegram models.Base.metadata.create_all(bind=engine) @@ -17,6 +20,9 @@ app.include_router(profile.router) app.include_router(event.router) app.include_router(agenda.router) +app.include_router(telegram.router) + +pylander.set_webhook() @app.get("/") diff --git a/app/routers/profile.py b/app/routers/profile.py index 3d73a481..901bc227 100644 --- a/app/routers/profile.py +++ b/app/routers/profile.py @@ -64,8 +64,7 @@ async def update_user_fullname( session.commit() url = router.url_path_for("profile") - response = RedirectResponse(url=url, status_code=HTTP_302_FOUND) - return response + return RedirectResponse(url=url, status_code=HTTP_302_FOUND) @router.post("/update_user_email") @@ -116,6 +115,22 @@ async def upload_user_photo( return RedirectResponse(url=url, status_code=HTTP_302_FOUND) +@router.post("/update_telegram_id") +async def update_telegram_id( + request: Request, session=Depends(get_db)): + + user = session.query(User).filter_by(id=1).first() + data = await request.form() + new_telegram_id = data['telegram_id'] + + # Update database + user.telegram_id = new_telegram_id + session.commit() + + url = router.url_path_for("profile") + return RedirectResponse(url=url, status_code=HTTP_302_FOUND) + + async def process_image(image, user): img = Image.open(io.BytesIO(image)) width, height = img.size diff --git a/app/routers/telegram.py b/app/routers/telegram.py new file mode 100644 index 00000000..b00e1c80 --- /dev/null +++ b/app/routers/telegram.py @@ -0,0 +1,41 @@ +from fastapi import APIRouter, Depends, Request +import json +import requests +from starlette.responses import RedirectResponse +from starlette.status import HTTP_302_FOUND + +from app.database.database import get_db +from app.database.models import User +from app.telegram.handlers import MessageHandler, reply_unknown_user +from app.telegram.models import Chat + + +router = APIRouter( + prefix="/telegram", + tags=["telegram"], + responses={404: {"description": "Not found"}}, +) + + +@router.get("/") +async def telegram(request: Request, session=Depends(get_db)): + + # todo: Add templating + return "Start using PyLander telegram bot!" + + +@router.post("/") +async def bot_client(request: Request, session=Depends(get_db)): + req = await request.json() + chat = Chat(req) + message = MessageHandler(chat) + + # Check if current chatter in DB + user = session.query(User).filter_by(telegram_id=chat.user_id).first() + + if user is None: + return await reply_unknown_user(chat) + else: + # Process reply + message.process_callback() + return user diff --git a/app/telegram/__init__.py b/app/telegram/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/telegram/handlers.py b/app/telegram/handlers.py new file mode 100644 index 00000000..042066c7 --- /dev/null +++ b/app/telegram/handlers.py @@ -0,0 +1,55 @@ +from .models import Chat +from .pylander import pylander + + +class MessageHandler: + COMMANDS = ['/start', '/logout', '/show_events', '/new_event'] + + def __init__(self, chat: Chat): + self.chat = chat + self.handlers = {} + for method in dir(self): + if method.endswith('_handler'): + method_name = method[:-len('_handler')] + self.handlers[method_name] = getattr(MessageHandler, method) + + def process_callback(self): + if self.chat.message in self.COMMANDS: + return self.handlers[self.chat.message[1:]](self) + elif (self.chat.message in self.handlers + and f'/{self.chat.message}' not in self.COMMANDS): + return self.handlers[self.chat.message](self) + else: + return self.default_handler() + + def default_handler(self): + answer = "Unknown command" + pylander.send_message(chat_id=self.chat.user_id, text=answer) + return answer + + def start_handler(self): + answer = f'''Hello, {self.chat.first_name}! +Welcome to Pylander telegram client!''' + pylander.send_message(chat_id=self.chat.user_id, text=answer) + return answer + + def show_events_handler(self): + answer = 'Choose events day' + pylander.send_message(chat_id=self.chat.user_id, text=answer) + return answer + + +async def reply_unknown_user(chat): + answer = f''' +Hello, {chat.first_name}! + +To use PyLander Bot you have to register +your Telegram Id in your profile page. + +Your Id is {chat.user_id} +Keep it secret! + +https://calendar.pythonic.guru/profile/ +''' + pylander.send_message(chat_id=chat.user_id, text=answer) + return answer diff --git a/app/telegram/keyboards.py b/app/telegram/keyboards.py new file mode 100644 index 00000000..e69de29b diff --git a/app/telegram/models.py b/app/telegram/models.py new file mode 100644 index 00000000..83d65412 --- /dev/null +++ b/app/telegram/models.py @@ -0,0 +1,40 @@ +import requests + + +class Chat: + def __init__(self, message): + self.message = message['message']['text'] + self.user_id = message['message']['from']['id'] + self.username = message['message']['from']['username'] + self.first_name = message['message']['from']['first_name'] + + +class Bot: + def __init__(self, bot_api, webhook_url): + self.base = self._set_base_url(bot_api) + self.webhook_setter_url = self._set_webhook_setter_url(webhook_url) + + def _set_base_url(self, bot_api): + return f'https://api.telegram.org/bot{bot_api}/' + + def _set_webhook_setter_url(self, webhook_url): + return f'{self.base}setWebhook?url={webhook_url}/telegram/' + + def set_webhook(self): + return requests.get(self.webhook_setter_url) + + def drop_webhook(self): + url = f'{self.base}deleteWebhook' + data = { + 'drop_pending_updates': True + } + return requests.get(url, data=data) + + def send_message(self, chat_id, text, reply_markup=[]): + message = { + 'chat_id': chat_id, + 'text': text, + 'reply_markup': reply_markup + } + url = f'{self.base}sendMessage' + return requests.post(url, data=message) diff --git a/app/telegram/pylander.py b/app/telegram/pylander.py new file mode 100644 index 00000000..89859c62 --- /dev/null +++ b/app/telegram/pylander.py @@ -0,0 +1,16 @@ +import os + +import os + +from dotenv import load_dotenv + +from app import config +from .models import Bot + +load_dotenv() + + +BOT_API = os.getenv("BOT_API", config.BOT_API) +WEBHOOK_URL = os.getenv("WEBHOOK_URL", config.WEBHOOK_URL) + +pylander = Bot(BOT_API, WEBHOOK_URL) diff --git a/app/templates/profile.html b/app/templates/profile.html index 9a0ddbda..0f42a5a1 100644 --- a/app/templates/profile.html +++ b/app/templates/profile.html @@ -21,25 +21,31 @@
  • +
  • +
  • +
  • @@ -131,6 +137,32 @@ + + + @@ -158,14 +190,18 @@
    {{ user.full_name }}

    - Features + Explore more features