From ba8e558c0d16bc189bd30788c43253376aea910b Mon Sep 17 00:00:00 2001 From: Teri-anric <2005ahi2005@gmail.com> Date: Wed, 23 Oct 2024 21:40:15 +0000 Subject: [PATCH 01/19] init telegram --- requirements.txt | 3 ++- web_app/api/telegram.py | 26 +++++++++++++++++++++++ web_app/telegram/__init__.py | 4 ++++ web_app/telegram/utils.py | 40 ++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 web_app/api/telegram.py create mode 100644 web_app/telegram/__init__.py create mode 100644 web_app/telegram/utils.py diff --git a/requirements.txt b/requirements.txt index 1e3f01d3..abcd0b2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,5 @@ psycopg2-binary==2.9.9 SQLAlchemy==2.0.35 pytest==8.3.3 pytest-asyncio==0.24.0 -httpx==0.27.2 \ No newline at end of file +httpx==0.27.2 +aiogram>=3.13.1 \ No newline at end of file diff --git a/web_app/api/telegram.py b/web_app/api/telegram.py new file mode 100644 index 00000000..aee71247 --- /dev/null +++ b/web_app/api/telegram.py @@ -0,0 +1,26 @@ +from typing import Literal + +from aiogram.types import Update +from fastapi import APIRouter, Request +from fastapi.responses import StreamingResponse +from web_app.db.crud import DBConnector +from web_app.telegram import bot, dp +from web_app.telegram.utils import build_response_writer + +router = APIRouter(include_in_schema=False) +db_connector = DBConnector() + + +@router.get("/api/webhook/telegram") +async def set_telegram_webhook(request: Request) -> Literal["ok"]: + url = str(request.url.replace(query="")) + await bot.set_webhook(url=url) + return "ok" + + +@router.post("/api/webhook/telegram") +async def telegram_webhook(update: Update): + result = await dp.feed_webhook_update(bot, update, db=db_connector) + return StreamingResponse( + build_response_writer(bot, result), media_type="multipart/form-data" + ) diff --git a/web_app/telegram/__init__.py b/web_app/telegram/__init__.py new file mode 100644 index 00000000..55390939 --- /dev/null +++ b/web_app/telegram/__init__.py @@ -0,0 +1,4 @@ +from aiogram import Bot, Dispatcher + +dp = Dispatcher() +bot = Bot() diff --git a/web_app/telegram/utils.py b/web_app/telegram/utils.py new file mode 100644 index 00000000..197a7621 --- /dev/null +++ b/web_app/telegram/utils.py @@ -0,0 +1,40 @@ +import secrets +from typing import Dict, Optional + +from aiogram import Bot +from aiogram.methods import TelegramMethod +from aiogram.methods.base import TelegramType +from aiogram.types import InputFile +from aiohttp import MultipartWriter + + +def build_response_writer( + bot: Bot, result: Optional[TelegramMethod[TelegramType]] +) -> MultipartWriter: + writer = MultipartWriter( + "form-data", + boundary=f"webhookBoundary{secrets.token_urlsafe(16)}", + ) + if not result: + return writer + + payload = writer.append(result.__api_method__) + payload.set_content_disposition("form-data", name="method") + + files: Dict[str, InputFile] = {} + for key, value in result.model_dump(warnings=False).items(): + value = bot.session.prepare_value(value, bot=bot, files=files) + if not value: + continue + payload = writer.append(value) + payload.set_content_disposition("form-data", name=key) + + for key, value in files.items(): + payload = writer.append(value.read(bot)) + payload.set_content_disposition( + "form-data", + name=key, + filename=value.filename or key, + ) + + return writer From 1f25dbaae230aa277ce1460074577b4ff4bff165 Mon Sep 17 00:00:00 2001 From: Teri-anric <2005ahi2005@gmail.com> Date: Thu, 24 Oct 2024 15:36:01 +0000 Subject: [PATCH 02/19] add docsstring --- web_app/api/telegram.py | 26 ++++++++++++++++++++++++++ web_app/telegram/__init__.py | 9 ++++++++- web_app/telegram/config.py | 4 ++++ web_app/telegram/utils.py | 11 +++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 web_app/telegram/config.py diff --git a/web_app/api/telegram.py b/web_app/api/telegram.py index aee71247..ce616cba 100644 --- a/web_app/api/telegram.py +++ b/web_app/api/telegram.py @@ -3,16 +3,30 @@ from aiogram.types import Update from fastapi import APIRouter, Request from fastapi.responses import StreamingResponse + from web_app.db.crud import DBConnector from web_app.telegram import bot, dp from web_app.telegram.utils import build_response_writer +# Create a FastAPI router for handling Telegram webhook requests router = APIRouter(include_in_schema=False) db_connector = DBConnector() @router.get("/api/webhook/telegram") async def set_telegram_webhook(request: Request) -> Literal["ok"]: + """ + Set the webhook for Telegram bot. + + This endpoint is called to set the webhook URL for the Telegram bot. + It extracts the URL from the request and sets it as the webhook. + + Args: + request (Request): The FastAPI request object. + + Returns: + Literal["ok"]: A confirmation message indicating the webhook has been set. + """ url = str(request.url.replace(query="")) await bot.set_webhook(url=url) return "ok" @@ -20,6 +34,18 @@ async def set_telegram_webhook(request: Request) -> Literal["ok"]: @router.post("/api/webhook/telegram") async def telegram_webhook(update: Update): + """ + Handle incoming updates from Telegram. + + This endpoint is called when Telegram sends an update to the webhook. + It processes the update and returns a streaming response. + + Args: + update (Update): The update object received from Telegram. + + Returns: + StreamingResponse: A streaming response containing the result of the update processing. + """ result = await dp.feed_webhook_update(bot, update, db=db_connector) return StreamingResponse( build_response_writer(bot, result), media_type="multipart/form-data" diff --git a/web_app/telegram/__init__.py b/web_app/telegram/__init__.py index 55390939..7f187fa6 100644 --- a/web_app/telegram/__init__.py +++ b/web_app/telegram/__init__.py @@ -1,4 +1,11 @@ from aiogram import Bot, Dispatcher +from .config import TELEGRAM_TOKEN + +# Initialize the Telegram bot (None for the ability to run without a bot token) +bot: Bot = None +if TELEGRAM_TOKEN: + bot = Bot(TELEGRAM_TOKEN) + +# Create a Dispatcher for handling updates dp = Dispatcher() -bot = Bot() diff --git a/web_app/telegram/config.py b/web_app/telegram/config.py new file mode 100644 index 00000000..668f4bc0 --- /dev/null +++ b/web_app/telegram/config.py @@ -0,0 +1,4 @@ +from os import getenv + +# Retrieve the Telegram bot token from environment variables +TELEGRAM_TOKEN = getenv("TELEGRAM_TOKEN") diff --git a/web_app/telegram/utils.py b/web_app/telegram/utils.py index 197a7621..b0b3c7ea 100644 --- a/web_app/telegram/utils.py +++ b/web_app/telegram/utils.py @@ -11,6 +11,16 @@ def build_response_writer( bot: Bot, result: Optional[TelegramMethod[TelegramType]] ) -> MultipartWriter: + """ + Build a MultipartWriter for sending a response to a Telegram webhook. + + Args: + bot (Bot): The instance of the Bot to use for handled requests. + result (Optional[TelegramMethod[TelegramType]]): The result of a Telegram method call. + + Returns: + MultipartWriter: A writer for the multipart/form-data request. + """ writer = MultipartWriter( "form-data", boundary=f"webhookBoundary{secrets.token_urlsafe(16)}", @@ -18,6 +28,7 @@ def build_response_writer( if not result: return writer + # Append the API method to the writer payload = writer.append(result.__api_method__) payload.set_content_disposition("form-data", name="method") From b195b09f019913042f61773eac3c7a1d5065e03c Mon Sep 17 00:00:00 2001 From: Teri-anric <2005ahi2005@gmail.com> Date: Thu, 24 Oct 2024 16:52:44 +0000 Subject: [PATCH 03/19] add start cmd to telegram bot --- web_app/telegram/__init__.py | 2 ++ web_app/telegram/__main__.py | 6 ++++++ web_app/telegram/config.py | 4 ++++ web_app/telegram/handlers/__init__.py | 1 + web_app/telegram/handlers/command.py | 13 +++++++++++++ web_app/telegram/markups.py | 10 ++++++++++ web_app/telegram/texts.py | 1 + 7 files changed, 37 insertions(+) create mode 100644 web_app/telegram/__main__.py create mode 100644 web_app/telegram/handlers/__init__.py create mode 100644 web_app/telegram/handlers/command.py create mode 100644 web_app/telegram/markups.py create mode 100644 web_app/telegram/texts.py diff --git a/web_app/telegram/__init__.py b/web_app/telegram/__init__.py index 7f187fa6..39ae7089 100644 --- a/web_app/telegram/__init__.py +++ b/web_app/telegram/__init__.py @@ -1,6 +1,7 @@ from aiogram import Bot, Dispatcher from .config import TELEGRAM_TOKEN +from .handlers import cmd_router # Initialize the Telegram bot (None for the ability to run without a bot token) bot: Bot = None @@ -9,3 +10,4 @@ # Create a Dispatcher for handling updates dp = Dispatcher() +dp.include_routers(cmd_router) diff --git a/web_app/telegram/__main__.py b/web_app/telegram/__main__.py new file mode 100644 index 00000000..2c43fd8a --- /dev/null +++ b/web_app/telegram/__main__.py @@ -0,0 +1,6 @@ +from web_app.db.crud import DBConnector + +from . import bot, dp + +if __name__ == "__main__": + dp.run_polling(bot, db=DBConnector()) diff --git a/web_app/telegram/config.py b/web_app/telegram/config.py index 668f4bc0..60864350 100644 --- a/web_app/telegram/config.py +++ b/web_app/telegram/config.py @@ -1,4 +1,8 @@ from os import getenv +from dotenv import load_dotenv + +load_dotenv() # Retrieve the Telegram bot token from environment variables TELEGRAM_TOKEN = getenv("TELEGRAM_TOKEN") +WEBAPP_URL = getenv("TELEGRAM_WEBAPP_URL") diff --git a/web_app/telegram/handlers/__init__.py b/web_app/telegram/handlers/__init__.py new file mode 100644 index 00000000..f27dff10 --- /dev/null +++ b/web_app/telegram/handlers/__init__.py @@ -0,0 +1 @@ +from .command import cmd_router diff --git a/web_app/telegram/handlers/command.py b/web_app/telegram/handlers/command.py new file mode 100644 index 00000000..afaa48e5 --- /dev/null +++ b/web_app/telegram/handlers/command.py @@ -0,0 +1,13 @@ +from aiogram import Router +from aiogram.filters import CommandStart +from aiogram.types import Message + +from ..markups import launch_main_web_app_kb +from ..texts import WELCOME_MESSAGE + +cmd_router = Router() + + +@cmd_router.message(CommandStart()) +async def start_cmd(message: Message): + return message.answer(WELCOME_MESSAGE, reply_markup=launch_main_web_app_kb) diff --git a/web_app/telegram/markups.py b/web_app/telegram/markups.py new file mode 100644 index 00000000..94e7a449 --- /dev/null +++ b/web_app/telegram/markups.py @@ -0,0 +1,10 @@ +from aiogram.types import (InlineKeyboardButton, InlineKeyboardMarkup, + WebAppInfo) + +from .config import WEBAPP_URL + +launch_main_web_app_kb = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Launch app", web_app=WebAppInfo(url=WEBAPP_URL))] + ] +) diff --git a/web_app/telegram/texts.py b/web_app/telegram/texts.py new file mode 100644 index 00000000..7b343df5 --- /dev/null +++ b/web_app/telegram/texts.py @@ -0,0 +1 @@ +WELCOME_MESSAGE = "Spotnet allows you to earn by using ETH collateral, borrowing USDC, and compounding the process. You can get started right away by clicking the button below to launch the web app! 🚀👇" From b9646f554da753a17c61e2587d12fb31f0cac3b0 Mon Sep 17 00:00:00 2001 From: Teri-anric <2005ahi2005@gmail.com> Date: Thu, 24 Oct 2024 16:58:31 +0000 Subject: [PATCH 04/19] add docsstring --- web_app/telegram/__init__.py | 2 ++ web_app/telegram/__main__.py | 1 + web_app/telegram/config.py | 1 + web_app/telegram/handlers/__init__.py | 2 ++ web_app/telegram/handlers/command.py | 10 ++++++++++ web_app/telegram/texts.py | 7 ++++++- 6 files changed, 22 insertions(+), 1 deletion(-) diff --git a/web_app/telegram/__init__.py b/web_app/telegram/__init__.py index 39ae7089..660ab0b9 100644 --- a/web_app/telegram/__init__.py +++ b/web_app/telegram/__init__.py @@ -6,8 +6,10 @@ # Initialize the Telegram bot (None for the ability to run without a bot token) bot: Bot = None if TELEGRAM_TOKEN: + # Create a Bot instance using the provided token bot = Bot(TELEGRAM_TOKEN) # Create a Dispatcher for handling updates dp = Dispatcher() +# Include command routers for handling specific commands dp.include_routers(cmd_router) diff --git a/web_app/telegram/__main__.py b/web_app/telegram/__main__.py index 2c43fd8a..b9c4540d 100644 --- a/web_app/telegram/__main__.py +++ b/web_app/telegram/__main__.py @@ -3,4 +3,5 @@ from . import bot, dp if __name__ == "__main__": + # Start polling for updates with the bot and database connector dp.run_polling(bot, db=DBConnector()) diff --git a/web_app/telegram/config.py b/web_app/telegram/config.py index 60864350..68c0ab07 100644 --- a/web_app/telegram/config.py +++ b/web_app/telegram/config.py @@ -1,4 +1,5 @@ from os import getenv + from dotenv import load_dotenv load_dotenv() diff --git a/web_app/telegram/handlers/__init__.py b/web_app/telegram/handlers/__init__.py index f27dff10..330c0b3d 100644 --- a/web_app/telegram/handlers/__init__.py +++ b/web_app/telegram/handlers/__init__.py @@ -1 +1,3 @@ from .command import cmd_router + +# Import command router for handling commands in the bot diff --git a/web_app/telegram/handlers/command.py b/web_app/telegram/handlers/command.py index afaa48e5..2c4acdda 100644 --- a/web_app/telegram/handlers/command.py +++ b/web_app/telegram/handlers/command.py @@ -5,9 +5,19 @@ from ..markups import launch_main_web_app_kb from ..texts import WELCOME_MESSAGE +# Create a router for handling commands cmd_router = Router() @cmd_router.message(CommandStart()) async def start_cmd(message: Message): + """ + Handle the /start command. + + Args: + message (Message): The incoming message containing the command. + + Returns: + None: Sends a welcome message with a button to launch the web app. + """ return message.answer(WELCOME_MESSAGE, reply_markup=launch_main_web_app_kb) diff --git a/web_app/telegram/texts.py b/web_app/telegram/texts.py index 7b343df5..54c1b7ba 100644 --- a/web_app/telegram/texts.py +++ b/web_app/telegram/texts.py @@ -1 +1,6 @@ -WELCOME_MESSAGE = "Spotnet allows you to earn by using ETH collateral, borrowing USDC, and compounding the process. You can get started right away by clicking the button below to launch the web app! 🚀👇" +# Welcome message to be sent to users when they start the bot +WELCOME_MESSAGE = ( + "Spotnet allows you to earn by using ETH collateral, " + "borrowing USDC, and compounding the process. You can get started " + "right away by clicking the button below to launch the web app! 🚀👇" +) From c05d3b243e433088f72cd496733bc2dc96338eb5 Mon Sep 17 00:00:00 2001 From: Teri-anric <2005ahi2005@gmail.com> Date: Sat, 26 Oct 2024 14:26:55 +0000 Subject: [PATCH 05/19] add telegram login compponent --- frontend/public/index.html | 3 + frontend/src/App.jsx | 3 +- .../src/components/Telegram/TelegramLogin.jsx | 42 +++++++++++++ .../src/components/Telegram/telegramLogin.css | 59 +++++++++++++++++++ frontend/src/components/header/Header.jsx | 6 +- frontend/src/components/header/header.css | 2 + 6 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/Telegram/TelegramLogin.jsx create mode 100644 frontend/src/components/Telegram/telegramLogin.css diff --git a/frontend/public/index.html b/frontend/public/index.html index f39b10b9..03f22420 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -9,6 +9,9 @@ + + + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2e0ace0b..e8f7ab63 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -10,6 +10,7 @@ import { connectWallet, logout } from './utils/wallet'; function App() { const [walletId, setWalletId] = useState(localStorage.getItem('wallet_id')); + const [tgUser, setTgUser] = useState(JSON.parse(localStorage.getItem('tg_user'))); const [error, setError] = useState(null); const navigate = useNavigate(); @@ -42,7 +43,7 @@ function App() { return (
-
+
{error &&
{error}
} diff --git a/frontend/src/components/Telegram/TelegramLogin.jsx b/frontend/src/components/Telegram/TelegramLogin.jsx new file mode 100644 index 00000000..b18784fd --- /dev/null +++ b/frontend/src/components/Telegram/TelegramLogin.jsx @@ -0,0 +1,42 @@ +import React, { useState, useEffect } from 'react'; +import './telegramLogin.css'; + +const TelegramLogin = ({ user, onLogin }) => { + useEffect(() => { + const initTelegramLogin = () => { + const tg = window.Telegram.WebApp; + tg.ready(); + + const user = tg.initDataUnsafe?.user; + if (user) { + onLogin(user); + } + }; + + initTelegramLogin(); + }, [onLogin]); + + const handleLogin = () => { + window.Telegram.Login.auth({ + bot_id: "BOT_ID", + request_access: 'write' + }, onLogin) + }; + window.console.log(user.photo_url); + return ( +
+ {user ? ( +
+ {user.first_name} + {user.first_name} +
+ ) : ( + + )} +
+ ); +}; + +export default TelegramLogin; diff --git a/frontend/src/components/Telegram/telegramLogin.css b/frontend/src/components/Telegram/telegramLogin.css new file mode 100644 index 00000000..30c8fac2 --- /dev/null +++ b/frontend/src/components/Telegram/telegramLogin.css @@ -0,0 +1,59 @@ +.telegram-login { + display: flex; + align-items: center; +} + +.user-info { + background: var(--gradient); + border-radius: 8px; + height: 52px; + width: 190px; + padding: 0 10px; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 10px; +} + +.user-photo { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; +} + +.user-name { + color: var(--black); + font-size: 16px; + font-family: var(--text-font); + font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.btn-telegram { + background: var(--button-gradient); + color: var(--black); + border: none; + height: 52px; + padding: 0 20px; + border-radius: 8px; + cursor: pointer; + font-size: 20px; + font-family: var(--text-font); + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; +} + +.btn-telegram:hover { + /* Змінюємо ефект при наведенні */ + background: var(--button-gradient-hover); +} + +.btn-telegram:active { + /* Додаємо ефект при натисканні */ + background: var(--button-gradient-active); +} diff --git a/frontend/src/components/header/Header.jsx b/frontend/src/components/header/Header.jsx index d1abd773..fc16db73 100644 --- a/frontend/src/components/header/Header.jsx +++ b/frontend/src/components/header/Header.jsx @@ -2,8 +2,9 @@ import React from 'react'; import './header.css' import { ReactComponent as Logo } from "../../assets/images/logo.svg"; import { Link } from 'react-router-dom'; +import TelegramLogin from '../Telegram/TelegramLogin'; -function Header({ walletId, onConnectWallet, onLogout }) { +function Header({ walletId, onConnectWallet, onLogout, tgUser, setTgUser }) { return (