Skip to content

Commit eb7dab0

Browse files
authored
Merge pull request #99 from ilyarolf/develop
Develop
2 parents 44e54d2 + 2d44247 commit eb7dab0

40 files changed

+1014
-642
lines changed

.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ BOT_LANGUAGE = ""
1313
MULTIBOT = ""
1414
ETHPLORER_API_KEY = ""
1515
CURRENCY = ""
16-
RUNTIME_ENVIRONMENT = "dev"
16+
RUNTIME_ENVIRONMENT = "DEV"
17+
WEBHOOK_SECRET_TOKEN = ""

bot.py

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,79 @@
11
import logging
2+
import traceback
3+
24
from aiogram.client.default import DefaultBotProperties
5+
from aiogram.fsm.storage.redis import RedisStorage
6+
from aiogram.types import BufferedInputFile
7+
from redis.asyncio import Redis
38
import config
49
from aiogram import Bot, Dispatcher
510
from aiogram.enums import ParseMode
6-
from aiogram.fsm.storage.memory import MemoryStorage
7-
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
8-
from aiohttp import web
9-
10-
from config import TOKEN, WEBHOOK_URL, ADMIN_ID_LIST
11+
from fastapi import FastAPI, Request, status, HTTPException
12+
from config import TOKEN, WEBHOOK_URL, ADMIN_ID_LIST, WEBHOOK_SECRET_TOKEN
1113
from db import create_db_and_tables
14+
import uvicorn
15+
from fastapi.responses import JSONResponse
16+
from services.notification import NotificationService
1217

18+
redis = Redis()
1319
bot = Bot(TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
14-
dp = Dispatcher(storage=MemoryStorage())
20+
dp = Dispatcher(storage=RedisStorage(redis))
21+
app = FastAPI()
22+
23+
24+
@app.post(config.WEBHOOK_PATH)
25+
async def webhook(request: Request):
26+
secret_token = request.headers.get("X-Telegram-Bot-Api-Secret-Token")
27+
if secret_token != WEBHOOK_SECRET_TOKEN:
28+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized")
1529

30+
try:
31+
update_data = await request.json()
32+
await dp.feed_webhook_update(bot, update_data)
33+
return {"status": "ok"}
34+
except Exception as e:
35+
logging.error(f"Error processing webhook: {e}")
36+
return {"status": "error"}, status.HTTP_500_INTERNAL_SERVER_ERROR
1637

17-
async def on_startup(bot: Bot):
38+
39+
@app.on_event("startup")
40+
async def on_startup():
1841
await create_db_and_tables()
19-
await bot.set_webhook(WEBHOOK_URL)
42+
await bot.set_webhook(
43+
url=WEBHOOK_URL,
44+
secret_token=WEBHOOK_SECRET_TOKEN
45+
)
2046
for admin in ADMIN_ID_LIST:
2147
try:
2248
await bot.send_message(admin, 'Bot is working')
2349
except Exception as e:
2450
logging.warning(e)
2551

2652

27-
async def on_shutdown(dp):
53+
@app.on_event("shutdown")
54+
async def on_shutdown():
2855
logging.warning('Shutting down..')
29-
3056
await bot.delete_webhook()
3157
await dp.storage.close()
32-
await dp.storage.wait_closed()
33-
3458
logging.warning('Bye!')
3559

3660

37-
def main() -> None:
38-
dp.startup.register(on_startup)
39-
app = web.Application()
40-
webhook_requests_handler = SimpleRequestHandler(
41-
dispatcher=dp,
42-
bot=bot
61+
@app.exception_handler(Exception)
62+
async def exception_handler(request: Request, exc: Exception):
63+
traceback_str = traceback.format_exc()
64+
admin_notification = (
65+
f"Critical error caused by {exc}\n\n"
66+
f"Stack trace:\n{traceback_str}"
67+
)
68+
if len(admin_notification) > 4096:
69+
byte_array = bytearray(admin_notification, 'utf-8')
70+
admin_notification = BufferedInputFile(byte_array, "exception.txt")
71+
await NotificationService.send_to_admins(admin_notification, None)
72+
return JSONResponse(
73+
status_code=500,
74+
content={"message": f"An error occurred: {str(exc)}"},
4375
)
44-
webhook_requests_handler.register(app, path=config.WEBHOOK_PATH)
45-
setup_application(app, dp, bot=bot)
46-
web.run_app(app, host=config.WEBAPP_HOST, port=config.WEBAPP_PORT)
76+
77+
78+
def main() -> None:
79+
uvicorn.run(app, host=config.WEBAPP_HOST, port=config.WEBAPP_PORT)

config.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
from dotenv import load_dotenv
44

55
from enums.currency import Currency
6+
from enums.runtime_environment import RuntimeEnvironment
67
from external_ip import get_sslipio_external_url
78
from ngrok_executor import start_ngrok
89

910
load_dotenv(".env")
10-
11-
if os.environ.get("RUNTIME_ENVIRONMENT") == "dev":
11+
RUNTIME_ENVIRONMENT = RuntimeEnvironment(os.environ.get("RUNTIME_ENVIRONMENT"))
12+
if RUNTIME_ENVIRONMENT == RuntimeEnvironment.DEV:
1213
WEBHOOK_HOST = start_ngrok()
1314
else:
1415
WEBHOOK_HOST = get_sslipio_external_url()
@@ -28,3 +29,4 @@
2829
MULTIBOT = os.environ.get("MULTIBOT", False) == 'true'
2930
ETHPLORER_API_KEY = os.environ.get("ETHPLORER_API_KEY")
3031
CURRENCY = Currency(os.environ.get("CURRENCY"))
32+
WEBHOOK_SECRET_TOKEN = os.environ.get("WEBHOOK_SECRET_TOKEN")

crypto_api/CryptoApiManager.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from datetime import datetime, timedelta
22
import aiohttp
3+
from sqlalchemy.ext.asyncio import AsyncSession
4+
from sqlalchemy.orm import Session
5+
36
import config
47
from enums.cryptocurrency import Cryptocurrency
58
from models.deposit import DepositDTO
@@ -19,7 +22,7 @@ async def fetch_api_request(url: str, params: dict | None = None) -> dict:
1922
return data
2023

2124
@staticmethod
22-
async def get_new_btc_deposits(user_dto: UserDTO, deposits) -> float:
25+
async def get_new_btc_deposits(user_dto: UserDTO, deposits: list[DepositDTO], session: AsyncSession | Session) -> float:
2326
url = f'https://mempool.space/api/address/{user_dto.btc_address}/utxo'
2427
data = await CryptoApiManager.fetch_api_request(url)
2528
deposits = [deposit.tx_id for deposit in deposits if deposit.network == "BTC"]
@@ -33,12 +36,12 @@ async def get_new_btc_deposits(user_dto: UserDTO, deposits) -> float:
3336
amount=deposit['value'],
3437
vout=deposit['vout']
3538
)
36-
await DepositService.create(deposit_dto)
39+
await DepositService.create(deposit_dto, session)
3740
deposit_sum += float(deposit["value"]) / 100_000_000
3841
return deposit_sum
3942

4043
@staticmethod
41-
async def get_new_ltc_deposits(user_dto: UserDTO, deposits) -> float:
44+
async def get_new_ltc_deposits(user_dto: UserDTO, deposits: list[DepositDTO], session: AsyncSession | Session) -> float:
4245
url = f"https://api.blockcypher.com/v1/ltc/main/addrs/{user_dto.ltc_address}"
4346
params = {"unspentOnly": "true"}
4447
data = await CryptoApiManager.fetch_api_request(url, params=params)
@@ -54,12 +57,12 @@ async def get_new_ltc_deposits(user_dto: UserDTO, deposits) -> float:
5457
amount=deposit['value'],
5558
vout=deposit['tx_output_n']
5659
)
57-
await DepositService.create(deposit_dto)
60+
await DepositService.create(deposit_dto, session)
5861
deposits_sum += float(deposit['value']) / 100_000_000
5962
return deposits_sum
6063

6164
@staticmethod
62-
async def get_sol_balance(user_dto: UserDTO, deposits) -> float:
65+
async def get_sol_balance(user_dto: UserDTO, deposits: list[DepositDTO], session: AsyncSession | Session) -> float:
6366
url = f"https://api.solana.fm/v0/accounts/{user_dto.sol_address}/transfers"
6467
data = await CryptoApiManager.fetch_api_request(url)
6568
deposits = [deposit.tx_id for deposit in deposits if deposit.network == "SOL"]
@@ -78,12 +81,12 @@ async def get_sol_balance(user_dto: UserDTO, deposits) -> float:
7881
amount=transfer['amount'],
7982
vout=transfer['instructionIndex']
8083
)
81-
await DepositService.create(deposit_dto)
84+
await DepositService.create(deposit_dto, session)
8285
deposits_sum += float(transfer['amount'] / 1_000_000_000)
8386
return deposits_sum
8487

8588
@staticmethod
86-
async def get_usdt_trc20_balance(user_dto: UserDTO, deposits) -> float:
89+
async def get_usdt_trc20_balance(user_dto: UserDTO, deposits: list[DepositDTO], session: AsyncSession | Session) -> float:
8790
url = f"https://api.trongrid.io/v1/accounts/{user_dto.trx_address}/transactions/trc20"
8891
params = {"only_confirmed": "true",
8992
"min_timestamp": CryptoApiManager.min_timestamp,
@@ -102,12 +105,12 @@ async def get_usdt_trc20_balance(user_dto: UserDTO, deposits) -> float:
102105
token_name='USDT_TRC20',
103106
amount=deposit['value'],
104107
)
105-
await DepositService.create(deposit_dto)
108+
await DepositService.create(deposit_dto, session)
106109
deposits_sum += float(deposit['value']) / pow(10, deposit['token_info']['decimals'])
107110
return deposits_sum
108111

109112
@staticmethod
110-
async def get_usdt_erc20_balance(user_dto: UserDTO, deposits) -> float:
113+
async def get_usdt_erc20_balance(user_dto: UserDTO, deposits: list[DepositDTO], session: AsyncSession | Session) -> float:
111114
# TODO(Combine the function to obtain erc20 tokens.)
112115
url = f'https://api.ethplorer.io/getAddressHistory/{user_dto.eth_address}'
113116
params = {
@@ -129,12 +132,12 @@ async def get_usdt_erc20_balance(user_dto: UserDTO, deposits) -> float:
129132
token_name='USDT_ERC20',
130133
amount=deposit['value']
131134
)
132-
await DepositService.create(deposit_dto)
135+
await DepositService.create(deposit_dto, session)
133136
deposits_sum += float(deposit['value']) / pow(10, 6)
134137
return deposits_sum
135138

136139
@staticmethod
137-
async def get_usdc_erc20_balance(user_dto: UserDTO, deposits):
140+
async def get_usdc_erc20_balance(user_dto: UserDTO, deposits: list[DepositDTO], session: AsyncSession | Session):
138141
# TODO(Combine the function to obtain erc20 tokens.)
139142
url = f'https://api.ethplorer.io/getAddressHistory/{user_dto.eth_address}'
140143
params = {
@@ -156,7 +159,7 @@ async def get_usdc_erc20_balance(user_dto: UserDTO, deposits):
156159
token_name='USDC_ERC20',
157160
amount=deposit['value']
158161
)
159-
await DepositService.create(deposit_dto)
162+
await DepositService.create(deposit_dto, session)
160163
deposits_sum += float(deposit['value']) / pow(10, 6)
161164
return deposits_sum
162165

@@ -175,18 +178,19 @@ async def get_crypto_prices(cryptocurrency: Cryptocurrency) -> float:
175178
return float(next(iter(response_json['result'].values()))['c'][0])
176179

177180
@staticmethod
178-
async def get_new_deposits_amount(user_dto: UserDTO, cryptocurrency: Cryptocurrency):
179-
deposits = await DepositService.get_by_user_dto(user_dto)
181+
async def get_new_deposits_amount(user_dto: UserDTO, cryptocurrency: Cryptocurrency,
182+
session: AsyncSession | Session):
183+
deposits = await DepositService.get_by_user_dto(user_dto, session)
180184
match cryptocurrency:
181185
case Cryptocurrency.BTC:
182-
return await CryptoApiManager.get_new_btc_deposits(user_dto, deposits)
186+
return await CryptoApiManager.get_new_btc_deposits(user_dto, deposits, session)
183187
case Cryptocurrency.LTC:
184-
return await CryptoApiManager.get_new_ltc_deposits(user_dto, deposits)
188+
return await CryptoApiManager.get_new_ltc_deposits(user_dto, deposits, session)
185189
case Cryptocurrency.SOL:
186-
return await CryptoApiManager.get_sol_balance(user_dto, deposits)
190+
return await CryptoApiManager.get_sol_balance(user_dto, deposits, session)
187191
case Cryptocurrency.USDT_TRC20:
188-
return await CryptoApiManager.get_usdt_trc20_balance(user_dto, deposits)
192+
return await CryptoApiManager.get_usdt_trc20_balance(user_dto, deposits, session)
189193
case Cryptocurrency.USDT_ERC20:
190-
return await CryptoApiManager.get_usdt_erc20_balance(user_dto, deposits)
194+
return await CryptoApiManager.get_usdt_erc20_balance(user_dto, deposits, session)
191195
case Cryptocurrency.USDC_ERC20:
192-
return await CryptoApiManager.get_usdc_erc20_balance(user_dto, deposits)
196+
return await CryptoApiManager.get_usdc_erc20_balance(user_dto, deposits, session)

db.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ async def session_execute(stmt, session: AsyncSession | Session) -> Result[Any]
7373
return query_result
7474

7575

76-
async def session_refresh(session: AsyncSession | Session, instance: object) -> None:
76+
async def session_flush(session: AsyncSession | Session) -> None:
7777
if isinstance(session, AsyncSession):
78-
await session.refresh(instance)
78+
await session.flush()
7979
else:
80-
session.refresh(instance)
80+
session.flush()
8181

8282

8383
async def session_commit(session: AsyncSession | Session) -> None:
@@ -91,6 +91,8 @@ async def session_commit(session: AsyncSession | Session) -> None:
9191
def set_sqlite_pragma(dbapi_connection, connection_record):
9292
cursor = dbapi_connection.cursor()
9393
cursor.execute("PRAGMA foreign_keys=ON")
94+
if config.DB_ENCRYPTION:
95+
cursor.execute("PRAGMA journal_mode=WAL")
9496
cursor.close()
9597

9698

docker-compose.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ services:
3535
MULTIBOT: "false" # Allows the use of a multibot
3636
ETHPLORER_API_KEY: "" # API key from Ethplorer
3737
CURRENCY: "USD" # fiat currency
38-
RUNTIME_ENVIRONMENT: "prod"
38+
RUNTIME_ENVIRONMENT: "PROD"
39+
WEBHOOK_SECRET_TOKEN: "1234567890" # Any string you want
3940
labels:
4041
caddy: YOUR-DOMAIN-GOES-HERE
4142
caddy.reverse_proxy: "bot:5000"
@@ -51,11 +52,21 @@ services:
5152
- 5000 # ${WEBAPP_PORT}
5253
volumes:
5354
- ./AiogramShopBot:/bot/data # endswith your database name
54-
command: ["python", "-u", "run.py"]
55+
command: [ "python", "-u", "run.py" ]
56+
57+
redis:
58+
image: redis:latest
59+
container_name: redis
60+
ports:
61+
- "6379:6379"
62+
volumes:
63+
- redis_data:/data
64+
restart: always
5565

5666
volumes:
5767
AiogramShopBot:
5868
caddy_data:
69+
redis_data:
5970

6071
networks:
6172
caddy:

enums/cryptocurrency.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from enum import Enum
22

33

4-
class Cryptocurrency(Enum):
4+
class Cryptocurrency(str, Enum):
55
BTC = "BTC"
66
LTC = "LTC"
77
SOL = "SOL"

enums/runtime_environment.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from enum import Enum
2+
3+
4+
class RuntimeEnvironment(str, Enum):
5+
DEV = "DEV"
6+
PROD = "PROD"

handlers/admin/admin.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@
2424

2525
@admin_router.message(F.text == Localizator.get_text(BotEntity.ADMIN, "menu"), AdminIdFilter())
2626
async def admin_command_handler(message: types.message):
27-
await admin(message)
27+
await admin(message=message)
2828

2929

30-
async def admin(message: Message | CallbackQuery):
30+
async def admin(**kwargs):
31+
message = kwargs.get("message") or kwargs.get("callback")
3132
admin_menu_builder = InlineKeyboardBuilder()
3233
admin_menu_builder.button(text=Localizator.get_text(BotEntity.ADMIN, "announcements"),
3334
callback_data=AdminAnnouncementCallback.create(level=0))
@@ -58,7 +59,10 @@ async def admin_menu_navigation(callback: CallbackQuery, state: FSMContext, call
5859
}
5960

6061
current_level_function = levels[current_level]
61-
if inspect.getfullargspec(current_level_function).annotations.get("state") == FSMContext:
62-
await current_level_function(callback, state)
63-
else:
64-
await current_level_function(callback)
62+
63+
kwargs = {
64+
"callback": callback,
65+
"state": state,
66+
}
67+
68+
await current_level_function(**kwargs)

0 commit comments

Comments
 (0)