diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/README-ua.md b/README-ua.md old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755 index 0f91ca9..4219551 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ aiofiles==23.1.0 -aiogram==2.24 +aiogram==3.1.1 aiohttp==3.8.5 aiosignal==1.3.1 aiosqlite==0.19.0 diff --git a/src/bot/app.py b/src/bot/app.py old mode 100644 new mode 100755 index 7d3c886..1d1e996 --- a/src/bot/app.py +++ b/src/bot/app.py @@ -1,20 +1,40 @@ -# -*- coding: utf-8 -*- +import asyncio -from aiogram import Dispatcher, executor -from database import \ - create_schema_if_not_exist as database_create_schema_if_not_exist -from loader import dp, tasks_scheduler +from database import create_schema_if_not_exist as database_create_schema_if_not_exist +from handlers import ( + admin_router, + balance_router, + channels_join_requests_router, + check_subscription_router, + close_functionality_router, + payment_router, + referral_router, + start_router, +) +from loader import bot, dp, tasks_scheduler +from middlewares import CreateUserMiddleware, UpdateLoggerMiddleware -async def on_startup(dp: Dispatcher): - import filters - import handlers - import middlewares - +async def on_startup(): + dp.update.outer_middleware(UpdateLoggerMiddleware()) + dp.message.outer_middleware(CreateUserMiddleware()) + + dp.include_routers( + admin_router, + start_router, + payment_router, + close_functionality_router, + channels_join_requests_router, + check_subscription_router, + referral_router, + balance_router, + ) + await database_create_schema_if_not_exist() tasks_scheduler.start() + await dp.start_polling(bot) + - if __name__ == "__main__": - executor.start_polling(dp, on_startup=on_startup, skip_updates=True) + asyncio.run(on_startup()) diff --git a/src/bot/data/__init__.py b/src/bot/data/__init__.py old mode 100644 new mode 100755 diff --git a/src/bot/data/config.py b/src/bot/data/config.py old mode 100644 new mode 100755 index 9f14ac7..41ff9b2 --- a/src/bot/data/config.py +++ b/src/bot/data/config.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import os from pathlib import Path @@ -10,21 +8,53 @@ BOT_TOKEN = env.str("BOT_TOKEN") -database_filename = 'database.db' -schema_filename = 'database_schema.sql' +database_filename = "database.db" +schema_filename = "database_schema.sql" project_filepath = Path(__file__).resolve().parent.parent.parent -sqlite_database_filepath = os.path.join(project_filepath, 'db', database_filename) -sqlite_schema_filepath = os.path.join(project_filepath, 'db', schema_filename) +sqlite_database_filepath = os.path.join(project_filepath, "db", database_filename) +sqlite_schema_filepath = os.path.join(project_filepath, "db", schema_filename) + +USDT_TRC20_WALLET_ADDRESS = "TF8aSMqpwtniPN77wS2EZTTcUKaaJhyorb" -USDT_TRC20_WALLET_ADDRESS = 'TF8aSMqpwtniPN77wS2EZTTcUKaaJhyorb' -SUBSCRIBE_AMOUNT_IN_USDT_TRC20 = 5 +# Key - count months +# Value - subscribe amount +# You can customise this dict +SUBSCRIBE_AMOUNT_BY_PLANS = { + 1: 5, + # 3: 15, + # 6: 30, + # 7: 31, +} NUMBER_DAYS_FROM_ONE_PAYMENT = 30 +SUBSCRIBE_END_NOTIFICATION_DAYS = [7, 3, 1] +REFERAL_REWARD = 5 +ADMINS_ID_LIST = [] private_channels = { - 'Channel 1': { - 'id': -100123456789, - 'invite_url': 'https://t.me/+ABCDEFGHIJKL' - }, + "Channel 1": {"id": -100123456789, "invite_url": "https://t.me/+ABCDEFGHIJKL"}, + "Channel 2": {"id": -100123456789, "invite_url": "https://t.me/+ABCDEFGHIJKL"}, + "Channel 3": {"id": -100123456789, "invite_url": "https://t.me/+ABCDEFGHIJKL"}, } + +""" + Use HTML to format text + + bold, bold + italic, italic + underline, underline + strikethrough, strikethrough, strikethrough + spoiler, spoiler + bold italic bold italic bold strikethrough italic bold strikethrough spoiler underline italic bold bold + inline URL + inline mention of a user + 👍 + inline fixed-width code +
pre-formatted fixed-width code block
+
pre-formatted fixed-width code block written in the Python programming language
+
Block quotation started\nBlock quotation continued\nThe last line of the block quotation
+ + And user \n to print the next text on a new line +""" +MAILING_TEXT = "Hello" diff --git a/src/bot/database/__init__.py b/src/bot/database/__init__.py old mode 100644 new mode 100755 diff --git a/src/bot/database/models.py b/src/bot/database/models.py old mode 100644 new mode 100755 index 18c00a4..78d6a7c --- a/src/bot/database/models.py +++ b/src/bot/database/models.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import dataclasses from typing import Optional @@ -11,15 +9,19 @@ class User: first_name: Optional[str] last_name: Optional[str] username: Optional[str] - days_sub_end: int - + days_sub_end: str + balance: int + referrer_id: int + @staticmethod def get_fields_for_sql_query(): - return '(%s)' % ', '.join([ field.name for field in dataclasses.fields(User) ][1:]) + return "(%s)" % ", ".join( + [field.name for field in dataclasses.fields(User)][1:] + ) @staticmethod def get_table_name(): - return 'Users' + return "Users" @dataclasses.dataclass() @@ -28,12 +30,15 @@ class Transaction: txid: str owner_telegram_id: int status: bool + months: int created_at_timestamp: int - + @staticmethod def get_fields_for_sql_query(): - return '(%s)' % ', '.join([ field.name for field in dataclasses.fields(Transaction) ][1:]) - + return "(%s)" % ", ".join( + [field.name for field in dataclasses.fields(Transaction)][1:] + ) + @staticmethod def get_table_name(): - return 'Transactions' + return "Transactions" diff --git a/src/bot/database/transactions.py b/src/bot/database/transactions.py old mode 100644 new mode 100755 index 419502f..95a0251 --- a/src/bot/database/transactions.py +++ b/src/bot/database/transactions.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from datetime import datetime, timedelta from typing import List, Optional, Union @@ -9,56 +7,71 @@ from .models import Transaction -async def get(database_id: Optional[int] = None, - txid: Optional[str] = None) -> Union[Transaction, None]: +async def get( + database_id: Optional[int] = None, txid: Optional[str] = None +) -> Union[Transaction, None]: async with aiosqlite.connect(sqlite_database_filepath) as connection: if database_id is not None: - sql_query = "SELECT * FROM %s WHERE id=%s" % (Transaction.get_table_name(), database_id) + sql_query = "SELECT * FROM %s WHERE id=%s" % ( + Transaction.get_table_name(), + database_id, + ) elif txid is not None: - sql_query = "SELECT * FROM %s WHERE txid='%s'" % (Transaction.get_table_name(), txid) + sql_query = "SELECT * FROM %s WHERE txid='%s'" % ( + Transaction.get_table_name(), + txid, + ) else: return None - - cursor = await connection.execute(sql_query) + + cursor = await connection.execute(sql_query) row = await cursor.fetchone() - if row is None: return row - + if row is None: + return row + return Transaction(*row) -async def create(txid: str, user_telegram_id: int) -> None: +async def create(txid: str, user_telegram_id: int, months: int = 1) -> None: async with aiosqlite.connect(sqlite_database_filepath) as connection: - await connection.execute(""" + await connection.execute( + """ INSERT INTO %s %s VALUES - ('%s', %s, %s, %s) + ('%s', %s, %s, %s, %s) ; - """ % ( - Transaction.get_table_name(), - Transaction.get_fields_for_sql_query(), - txid, - user_telegram_id, - False, - int(datetime.now().timestamp()), - )) + """ + % ( + Transaction.get_table_name(), + Transaction.get_fields_for_sql_query(), + txid, + user_telegram_id, + False, + months, + int(datetime.now().timestamp()), + ) + ) await connection.commit() - - -async def set_status(status: bool, - database_id: Optional[int] = None, - txid: Optional[str] = None) -> None: - transaction = await get(database_id, txid) - if transaction is None: return None - + + +async def set_status( + status: bool, database_id: Optional[int] = None, txid: Optional[str] = None +) -> None: + transaction = await get(database_id, txid) + if transaction is None: + return None + async with aiosqlite.connect(sqlite_database_filepath) as connection: sql_query = "UPDATE %s SET status=%s WHERE id=%s" % ( - Transaction.get_table_name(), status, transaction.id, + Transaction.get_table_name(), + status, + transaction.id, ) - + await connection.execute(sql_query) await connection.commit() - + async def get_new() -> List[Transaction]: async with aiosqlite.connect(sqlite_database_filepath) as connection: @@ -69,10 +82,10 @@ async def get_new() -> List[Transaction]: """ % ( Transaction.get_table_name(), False, - int((datetime.now() - timedelta(minutes=20)).timestamp()) + int((datetime.now() - timedelta(minutes=20)).timestamp()), ) - - cursor = await connection.execute(sql_query) + + cursor = await connection.execute(sql_query) rows = await cursor.fetchall() - - return [ Transaction(*row) for row in rows ] + + return [Transaction(*row) for row in rows] diff --git a/src/bot/database/users.py b/src/bot/database/users.py old mode 100644 new mode 100755 index 6b996e4..7b6fc57 --- a/src/bot/database/users.py +++ b/src/bot/database/users.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from typing import List, Optional, Union import aiosqlite @@ -8,64 +6,127 @@ from .models import User -async def get(database_id: Optional[int] = None, - telegram_id: Optional[int] = None) -> Union[User, None]: +async def get( + database_id: Optional[int] = None, telegram_id: Optional[int] = None +) -> Union[User, None]: async with aiosqlite.connect(sqlite_database_filepath) as connection: if database_id is not None: - sql_query = "SELECT * FROM %s WHERE id=%s" % (User.get_table_name(), database_id) + sql_query = "SELECT * FROM %s WHERE id=%s" % ( + User.get_table_name(), + database_id, + ) elif telegram_id is not None: - sql_query = "SELECT * FROM %s WHERE telegram_id=%s" % (User.get_table_name(), telegram_id) + sql_query = "SELECT * FROM %s WHERE telegram_id=%s" % ( + User.get_table_name(), + telegram_id, + ) else: return None - + cursor = await connection.execute(sql_query) row = await cursor.fetchone() - if row is None: return row - + if row is None: + return row + return User(*row) -async def set_days_sub_end(count_days: int, - database_id: Optional[int] = None, - telegram_id: Optional[int] = None) -> None: +async def update_subscription_date( + date: str, + database_id: Optional[int] = None, + telegram_id: Optional[int] = None, +) -> None: user = await get(database_id, telegram_id) - if user is None: return None - + if user is None: + return None + async with aiosqlite.connect(sqlite_database_filepath) as connection: - sql_query = "UPDATE %s SET days_sub_end=%s WHERE id=%s" % ( - User.get_table_name(), count_days, user.id, + sql_query = "UPDATE %s SET days_sub_end='%s' WHERE id=%s" % ( + User.get_table_name(), + date, + user.id, ) - + await connection.execute(sql_query) await connection.commit() - - -async def create_if_not_exist(telegram_id: int, - firstname: str, - lastname: str, - username: str) -> None: + + +async def create_if_not_exist( + telegram_id: int, + firstname: Union[str, None], + lastname: Union[str, None], + username: Union[str, None], +) -> None: record = await get(telegram_id=telegram_id) if record is None: async with aiosqlite.connect(sqlite_database_filepath) as connection: - await connection.execute(""" + await connection.execute( + """ INSERT INTO %s %s VALUES - (%s, '%s', '%s', '%s', %s) + (%s, '%s', '%s', '%s', datetime('now'), %s, %s) ; - """ % ( - User.get_table_name(), - User.get_fields_for_sql_query(), - telegram_id, firstname, lastname, username, 0, - )) + """ + % ( + User.get_table_name(), + User.get_fields_for_sql_query(), + telegram_id, + firstname, + lastname, + username, + 0, + 0, + ) + ) await connection.commit() async def get_all() -> List[User]: async with aiosqlite.connect(sqlite_database_filepath) as connection: sql_query = "SELECT * FROM %s" % User.get_table_name() - - cursor = await connection.execute(sql_query) + + cursor = await connection.execute(sql_query) rows = await cursor.fetchall() - - return [ User(*row) for row in rows ] + + return [User(*row) for row in rows] + + +async def update_referrer_id( + referrer_id: int, + to_database_id: Optional[int] = None, + to_telegram_id: Optional[int] = None, +) -> None: + user = await get(to_database_id, to_telegram_id) + if user is None: + return + + async with aiosqlite.connect(sqlite_database_filepath) as connection: + sql_query = "UPDATE %s SET referrer_id=%s WHERE id=%s" % ( + User.get_table_name(), + referrer_id, + user.id, + ) + + await connection.execute(sql_query) + await connection.commit() + + +async def increase_balance_by( + points: int, + database_id: Optional[int] = None, + telegram_id: Optional[int] = None, +) -> None: + user = await get(database_id, telegram_id) + if user is None: + return + + async with aiosqlite.connect(sqlite_database_filepath) as connection: + sql_query = "UPDATE %s SET balance=%s WHERE id=%s" % ( + User.get_table_name(), + user.balance + points, + user.id, + ) + + await connection.execute(sql_query) + await connection.commit() diff --git a/src/bot/filters/__init__.py b/src/bot/filters/__init__.py old mode 100644 new mode 100755 index f6143cd..47d30ab --- a/src/bot/filters/__init__.py +++ b/src/bot/filters/__init__.py @@ -1,10 +1,3 @@ -# -*- coding: utf-8 -*- - -from loader import dp - +from .is_admin import IsAdminFilter from .user_not_subscribed import UserNotSubscribedFilter from .user_subscribed import UserSubscribedFilter - -if __name__ == "filters": - dp.filters_factory.bind(UserSubscribedFilter) - dp.filters_factory.bind(UserNotSubscribedFilter) diff --git a/src/bot/filters/is_admin.py b/src/bot/filters/is_admin.py new file mode 100644 index 0000000..af943f9 --- /dev/null +++ b/src/bot/filters/is_admin.py @@ -0,0 +1,14 @@ +from aiogram import types +from aiogram.filters import BaseFilter +from data.config import ADMINS_ID_LIST + + +class IsAdminFilter(BaseFilter): + + def __init__(self): + pass + + async def __call__(self, message: types.Message) -> bool: + if message.from_user is None: + return False + return message.from_user.id in ADMINS_ID_LIST diff --git a/src/bot/filters/user_not_subscribed.py b/src/bot/filters/user_not_subscribed.py old mode 100644 new mode 100755 index 6a99419..f8114b4 --- a/src/bot/filters/user_not_subscribed.py +++ b/src/bot/filters/user_not_subscribed.py @@ -1,14 +1,21 @@ -# -*- coding: utf-8 -*- +from datetime import datetime from aiogram import types -from aiogram.dispatcher.filters import Filter +from aiogram.filters import BaseFilter from database import users -class UserNotSubscribedFilter(Filter): - key = "user_not_subscribed" +class UserNotSubscribedFilter(BaseFilter): - async def check(self, message: types.Message): + def __init__(self): + pass + + async def __call__(self, message: types.Message) -> bool: + if message.from_user is None: + return False user = await users.get(telegram_id=message.from_user.id) - if user is None: return False - return user.days_sub_end <= 0 + if user is None: + return False + return datetime.now() > datetime.strptime( + user.days_sub_end, "%Y-%m-%d %H:%M:%S" + ) diff --git a/src/bot/filters/user_subscribed.py b/src/bot/filters/user_subscribed.py old mode 100644 new mode 100755 index 2797e85..4f4ff1a --- a/src/bot/filters/user_subscribed.py +++ b/src/bot/filters/user_subscribed.py @@ -1,14 +1,21 @@ -# -*- coding: utf-8 -*- +from datetime import datetime from aiogram import types -from aiogram.dispatcher.filters import Filter +from aiogram.filters import BaseFilter from database import users -class UserSubscribedFilter(Filter): - key = "user_subscribed" +class UserSubscribedFilter(BaseFilter): - async def check(self, message: types.Message): + def __init__(self): + pass + + async def __call__(self, message: types.Message) -> bool: + if message.from_user is None: + return False user = await users.get(telegram_id=message.from_user.id) - if user is None: return False - return user.days_sub_end >= 1 + if user is None: + return False + return datetime.now() <= datetime.strptime( + user.days_sub_end, "%Y-%m-%d %H:%M:%S" + ) diff --git a/src/bot/handlers/__init__.py b/src/bot/handlers/__init__.py old mode 100644 new mode 100755 index 8fbee22..eb59401 --- a/src/bot/handlers/__init__.py +++ b/src/bot/handlers/__init__.py @@ -1,6 +1,8 @@ -# -*- coding: utf-8 -*- - -from .channels_join_requests import * -from .close_functionality import * -from .payment import * -from .start import * +from .admin import admin_router +from .balance import balance_router +from .channels_join_requests import channels_join_requests_router +from .check_subscription import check_subscription_router +from .close_functionality import close_functionality_router +from .payment import payment_router +from .referral import referral_router +from .start import start_router diff --git a/src/bot/handlers/admin.py b/src/bot/handlers/admin.py new file mode 100644 index 0000000..99aac14 --- /dev/null +++ b/src/bot/handlers/admin.py @@ -0,0 +1,31 @@ +from datetime import datetime + +from aiogram import Router, types +from aiogram.filters import Command +from data.config import MAILING_TEXT +from database import users +from filters import IsAdminFilter +from loader import bot + +admin_router = Router() + + +@admin_router.message(IsAdminFilter(), Command("start_mailing")) +async def start_mailing_to_not_subscribed_users(message: types.Message): + """Start mailing to not subscribet users""" + + if message.from_user is None: + return + users_records = await users.get_all() + for user in users_records: + if user.telegram_id == message.from_user.id: + continue + if datetime.now() > datetime.strptime(user.days_sub_end, "%Y-%m-%d %H:%M:%S"): + await bot.send_message( + chat_id=user.telegram_id, + text=MAILING_TEXT, + ) + await message.answer( + text="The message was successfully sent to %s" + % (user.telegram_id, user.first_name) + ) diff --git a/src/bot/handlers/balance.py b/src/bot/handlers/balance.py new file mode 100644 index 0000000..80870c2 --- /dev/null +++ b/src/bot/handlers/balance.py @@ -0,0 +1,15 @@ +from aiogram import F, Router, types +from database import users + +balance_router = Router() + + +@balance_router.message(F.text == "Balance") +async def show_balance(message: types.Message): + if message.from_user is None: + return + user = await users.get(telegram_id=message.from_user.id) + if user is None: + return + + await message.answer(text=f"Your balance: {user.balance}") diff --git a/src/bot/handlers/channels_join_requests.py b/src/bot/handlers/channels_join_requests.py old mode 100644 new mode 100755 index 04c0bd4..421f1c8 --- a/src/bot/handlers/channels_join_requests.py +++ b/src/bot/handlers/channels_join_requests.py @@ -1,11 +1,17 @@ -from aiogram import types +from datetime import datetime + +from aiogram import Router, types from database import users -from loader import dp + +channels_join_requests_router = Router() -@dp.chat_join_request_handler() +@channels_join_requests_router.chat_join_request() async def private_channel_join_request(chat_join_request: types.ChatJoinRequest): - user = await users.get(telegram_id=chat_join_request.from_user.id) - if user is None: return - if user.days_sub_end >= 1: + user = await users.get(telegram_id=chat_join_request.from_user.id) + if user is None: + return + if datetime.now() < datetime.strptime(user.days_sub_end, "%Y-%m-%d %H:%M:%S"): await chat_join_request.approve() + else: + await chat_join_request.decline() diff --git a/src/bot/handlers/check_subscription.py b/src/bot/handlers/check_subscription.py new file mode 100644 index 0000000..f5ef0e0 --- /dev/null +++ b/src/bot/handlers/check_subscription.py @@ -0,0 +1,34 @@ +from datetime import datetime + +from aiogram import F, Router, types +from database import users + +check_subscription_router = Router() + + +@check_subscription_router.message(F.text == "Check subscription") +async def check_subscription(message: types.Message): + if message.from_user is None: + return + user_database_record = await users.get(telegram_id=message.from_user.id) + if user_database_record is None: + return + + datetime_now = datetime.now() + sub_date_end = datetime.strptime( + user_database_record.days_sub_end, "%Y-%m-%d %H:%M:%S" + ) + + # User have subscription + if datetime_now < sub_date_end: + await message.answer( + text="Your subscription is active until %s" + % user_database_record.days_sub_end + ) + + # User don't have subscription + elif datetime_now >= sub_date_end: + await message.answer( + text="Your subscription has expired %s" + % user_database_record.days_sub_end + ) diff --git a/src/bot/handlers/close_functionality.py b/src/bot/handlers/close_functionality.py old mode 100644 new mode 100755 index a6689dc..24f9548 --- a/src/bot/handlers/close_functionality.py +++ b/src/bot/handlers/close_functionality.py @@ -1,12 +1,16 @@ -from aiogram import types +from aiogram import F, Router, types from filters import UserSubscribedFilter from keyboards import inline as inline_keyboard -from loader import dp +close_functionality_router = Router() -@dp.message_handler(UserSubscribedFilter(), text='Show close functionality') + +@close_functionality_router.message( + UserSubscribedFilter(), + F.text == "Show close functionality", +) async def show_private_channels(message: types.Message): await message.answer( - text='You can subscribe to closed channels.', + text="You can subscribe to closed channels.", reply_markup=await inline_keyboard.channels(), ) diff --git a/src/bot/handlers/payment.py b/src/bot/handlers/payment.py old mode 100644 new mode 100755 index 2665c7f..6301aa5 --- a/src/bot/handlers/payment.py +++ b/src/bot/handlers/payment.py @@ -1,48 +1,69 @@ -# -*- coding: utf-8 -*- - -from aiogram import types -from aiogram.dispatcher import FSMContext -from data.config import (SUBSCRIBE_AMOUNT_IN_USDT_TRC20, - USDT_TRC20_WALLET_ADDRESS) +from aiogram import F, Router, types +from aiogram.fsm.context import FSMContext +from data.config import SUBSCRIBE_AMOUNT_BY_PLANS, USDT_TRC20_WALLET_ADDRESS from database import transactions -from filters.user_not_subscribed import UserNotSubscribedFilter from keyboards import reply as reply_keyboards -from loader import dp from statesgroup import GetTxidFromUser from utils import tronscan_service +payment_router = Router() + -@dp.message_handler(UserNotSubscribedFilter(), text='Make subscription') +@payment_router.message(F.text == "Make subscription") +@payment_router.message(F.text == "Renew subscription") async def make_subscription(message: types.Message): await message.answer( - text=f'To pay, use this USDT TC20 wallet: {USDT_TRC20_WALLET_ADDRESS}.\n' - f'Transfer {SUBSCRIBE_AMOUNT_IN_USDT_TRC20} USDT TRC20.\n' - 'After submitting, click the Confirm button.', + text=f"Choose subscription plan", + reply_markup=await reply_keyboards.subscription_termins( + SUBSCRIBE_AMOUNT_BY_PLANS.keys() + ), + ) + + +@payment_router.message(F.text.contains("month")) +async def set_subscribtion_termin(message: types.Message, state: FSMContext): + if message.text is None: + return + termin = int(message.text.split(" ")[0]) + await state.set_data({"subscription_termin": termin}) + await message.answer( + text=f"To pay, use this USDT TC20 wallet: {USDT_TRC20_WALLET_ADDRESS}.\n" + f"Transfer {SUBSCRIBE_AMOUNT_BY_PLANS[termin]} USDT TRC20.\n" + "After submitting, click the Confirm button.", reply_markup=await reply_keyboards.confirm_transfer(), ) - - -@dp.message_handler(UserNotSubscribedFilter(), text='Confirm transfer') -async def confirm_transfer(message: types.Message): - await GetTxidFromUser.state.set() + + +@payment_router.message(F.text == "Confirm transfer") +async def confirm_transfer(message: types.Message, state: FSMContext): + await state.set_state(GetTxidFromUser.state) await message.answer( - text='Great, send me the transaction txid to verify the transfer.', + text="Great, send me the transaction txid to verify the transfer.", reply_markup=types.ReplyKeyboardRemove(), ) - -@dp.message_handler(UserNotSubscribedFilter(), state=GetTxidFromUser.state) + +@payment_router.message(GetTxidFromUser.state) async def check_transaction(message: types.Message, state: FSMContext): transaction = await transactions.get(txid=message.text) - + + if message.text is None or message.from_user is None: + return + if transaction is None and tronscan_service.is_valid_transaction_hash(message.text): - await state.finish() - await transactions.create(message.text, message.from_user.id) + data = await state.get_data() + await transactions.create( + message.text, + message.from_user.id, + data["subscription_termin"], + ) + await state.clear() await message.answer( - text='Great, wait for the end of the transaction, ' - 'and I will notify you when the subscription is charged.', + text="Great, wait for the end of the transaction, " + "and I will notify you when the subscription is charged.", + reply_markup=await reply_keyboards.back_to_main_menu(), ) else: await message.answer( - text='Please send me a new transaction, or check the txid', + text="Please send me a new transaction, or check the txid", ) diff --git a/src/bot/handlers/referral.py b/src/bot/handlers/referral.py new file mode 100644 index 0000000..80487b1 --- /dev/null +++ b/src/bot/handlers/referral.py @@ -0,0 +1,18 @@ +from aiogram import F, Router, types +from database import users +from loader import bot + +referral_router = Router() + + +@referral_router.message(F.text == "Referral link") +async def referral_link(message: types.Message): + if message.from_user is None: + return + user = await users.get(telegram_id=message.from_user.id) + if user is None: + return + bot_data = await bot.get_me() + await message.answer( + text=f"Your referal link https://t.me/{bot_data.username}?start={user.id}", + ) diff --git a/src/bot/handlers/start.py b/src/bot/handlers/start.py old mode 100644 new mode 100755 index 16a3c59..f9e4ede --- a/src/bot/handlers/start.py +++ b/src/bot/handlers/start.py @@ -1,28 +1,50 @@ -# -*- coding: utf-8 -*- +from typing import Optional -from aiogram import types +from aiogram import F, Router, types +from aiogram.filters import CommandObject, CommandStart from database import users from filters.user_not_subscribed import UserNotSubscribedFilter from filters.user_subscribed import UserSubscribedFilter from keyboards import reply as reply_keyboards -from loader import dp +start_router = Router() -@dp.message_handler(UserSubscribedFilter(), commands=['start'], state="*") + +@start_router.message(UserSubscribedFilter(), CommandStart()) +@start_router.message(UserSubscribedFilter(), F.text == "Back to main menu") async def start_for_subsribed_user(message: types.Message): + if message.from_user is None: + return user = await users.get(telegram_id=message.from_user.id) - if user is None:return - + if user is None: + return + await message.answer( - text='Hello.\n' - f'There are {user.days_sub_end} days left until the end of the subscription. \n' - 'Do not miss the day of payment to always have access to closed functionality.', + text="Hello.\n" + f"Your subscription is active until {user.days_sub_end}.\n" + "Do not miss the day of payment to always have access to closed functionality.", reply_markup=await reply_keyboards.close_functionality(), ) - -@dp.message_handler(UserNotSubscribedFilter(), commands=['start'], state="*") -async def start_for_not_subsribed_user(message: types.Message): + + +@start_router.message(UserNotSubscribedFilter(), CommandStart()) +@start_router.message(UserNotSubscribedFilter(), F.text == "Back to main menu") +async def start_for_not_subsribed_user( + message: types.Message, command: Optional[CommandObject] = None +): + if ( + command is not None + and command.args is not None + and command.args.isdigit() + and message.from_user is not None + ): + user = await users.get(telegram_id=message.from_user.id) + if user is not None and user.referrer_id == 0 and command.args.isdigit(): + if user.id != int(command.args): + await users.update_referrer_id( + referrer_id=int(command.args), to_database_id=user.id + ) await message.answer( - text='Hello. Subscribe to the bot to get access to the closed functionality.', + text="Hello. Subscribe to the bot to get access to the closed functionality.", reply_markup=await reply_keyboards.make_subscribtion(), ) diff --git a/src/bot/keyboards/__init__.py b/src/bot/keyboards/__init__.py old mode 100644 new mode 100755 diff --git a/src/bot/keyboards/inline.py b/src/bot/keyboards/inline.py old mode 100644 new mode 100755 index ea0e3f3..b9d8956 --- a/src/bot/keyboards/inline.py +++ b/src/bot/keyboards/inline.py @@ -1,16 +1,18 @@ from aiogram import types +from aiogram.utils.keyboard import InlineKeyboardBuilder from data.config import private_channels async def channels() -> types.InlineKeyboardMarkup: - keyboard = types.InlineKeyboardMarkup() - + builder = InlineKeyboardBuilder() + for name in private_channels.keys(): - keyboard.add( + builder.add( types.InlineKeyboardButton( text=name, - url=private_channels[name]['invite_url'], + url=private_channels[name]["invite_url"], ) ) + builder.adjust(1) - return keyboard + return builder.as_markup(resize_keyboard=True) diff --git a/src/bot/keyboards/reply.py b/src/bot/keyboards/reply.py old mode 100644 new mode 100755 index 7b4a8ac..b630044 --- a/src/bot/keyboards/reply.py +++ b/src/bot/keyboards/reply.py @@ -1,35 +1,62 @@ -# -*- coding: utf-8 -*- +from collections.abc import Iterable from aiogram import types -async def close_functionality(): - return types.ReplyKeyboardMarkup(resize_keyboard=True).add( - types.KeyboardButton( - text='Show close functionality', - ) +async def close_functionality() -> types.ReplyKeyboardMarkup: + return types.ReplyKeyboardMarkup( + keyboard=[ + [types.KeyboardButton(text="Balance")], + [types.KeyboardButton(text="Renew subscription")], + [types.KeyboardButton(text="Show close functionality")], + [types.KeyboardButton(text="Check subscription")], + [types.KeyboardButton(text="Referral link")], + ], + resize_keyboard=True, ) -async def make_subscribtion(): - return types.ReplyKeyboardMarkup(resize_keyboard=True).add( - types.KeyboardButton( - text='Make subscription', - ) +async def make_subscribtion() -> types.ReplyKeyboardMarkup: + return types.ReplyKeyboardMarkup( + keyboard=[ + [types.KeyboardButton(text="Balance")], + [types.KeyboardButton(text="Make subscription")], + [types.KeyboardButton(text="Check subscription")], + [types.KeyboardButton(text="Referral link")], + ], + resize_keyboard=True, ) -async def confirm_transfer(): - return types.ReplyKeyboardMarkup(resize_keyboard=True).add( - types.KeyboardButton( - text='Confirm transfer', - ) +async def confirm_transfer() -> types.ReplyKeyboardMarkup: + return types.ReplyKeyboardMarkup( + keyboard=[ + [types.KeyboardButton(text="Confirm transfer")], + [types.KeyboardButton(text="Back to main menu")], + ], + resize_keyboard=True, ) -async def check_transaction(): - return types.ReplyKeyboardMarkup(resize_keyboard=True).add( - types.KeyboardButton( - text='Check transaction', - ) +async def check_transaction() -> types.ReplyKeyboardMarkup: + return types.ReplyKeyboardMarkup( + keyboard=[[types.KeyboardButton(text="Check transaction")]], + resize_keyboard=True, + ) + + +async def back_to_main_menu() -> types.ReplyKeyboardMarkup: + return types.ReplyKeyboardMarkup( + keyboard=[[types.KeyboardButton(text="Back to main menu")]], + resize_keyboard=True, + ) + + +async def subscription_termins(plans: Iterable[int]) -> types.ReplyKeyboardMarkup: + return types.ReplyKeyboardMarkup( + keyboard=[ + [types.KeyboardButton(text=f"{plan} month") for plan in plans], + [types.KeyboardButton(text="Back to main menu")], + ], + resize_keyboard=True, ) diff --git a/src/bot/loader.py b/src/bot/loader.py old mode 100644 new mode 100755 index ffe4c71..7b0fff1 --- a/src/bot/loader.py +++ b/src/bot/loader.py @@ -1,22 +1,28 @@ import os -from aiogram import Bot, Dispatcher, types -from aiogram.contrib.fsm_storage.memory import MemoryStorage +from aiogram import Bot, Dispatcher +from aiogram.enums import ParseMode +from aiogram.fsm.storage.memory import MemoryStorage from apscheduler.schedulers.asyncio import AsyncIOScheduler from data.config import BOT_TOKEN from logzero import logfile, logger -from utils import (decrease_subscription_days, kick_users_from_channels, - subscription_checker) +from utils import ( + ban_users_from_channels, + decrease_subscription_days, + subscription_checker, +) -if not os.path.exists('logs/'): - os.system('mkdir logs') -logfile('logs/bot.log') +if not os.path.exists("logs/"): + os.system("mkdir logs") +logfile("logs/bot.log") -bot = Bot(token=BOT_TOKEN, parse_mode=types.ParseMode.HTML) +bot = Bot(token=BOT_TOKEN, parse_mode=ParseMode.HTML) storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) tasks_scheduler = AsyncIOScheduler() -tasks_scheduler.add_job(subscription_checker.task, 'interval', minutes=1, args=(bot, )) -tasks_scheduler.add_job(decrease_subscription_days.task, 'interval', days=1, args=(bot, )) -tasks_scheduler.add_job(kick_users_from_channels.task, 'interval', days=1, args=(bot, )) +tasks_scheduler.add_job(subscription_checker.task, "interval", minutes=1, args=(bot,)) +tasks_scheduler.add_job( + decrease_subscription_days.task, "interval", days=1, args=(bot,) +) +tasks_scheduler.add_job(ban_users_from_channels.task, "interval", days=1, args=(bot,)) diff --git a/src/bot/middlewares/__init__.py b/src/bot/middlewares/__init__.py old mode 100644 new mode 100755 index 55bdfb6..21f5344 --- a/src/bot/middlewares/__init__.py +++ b/src/bot/middlewares/__init__.py @@ -1,10 +1,2 @@ -# -*- coding: utf-8 -*- - -from loader import dp - from .create_user_middleware import CreateUserMiddleware from .logger_middleware import UpdateLoggerMiddleware - -if __name__ == "middlewares": - dp.middleware.setup(UpdateLoggerMiddleware()) - dp.middleware.setup(CreateUserMiddleware()) diff --git a/src/bot/middlewares/create_user_middleware.py b/src/bot/middlewares/create_user_middleware.py old mode 100644 new mode 100755 index 1303b99..d78898f --- a/src/bot/middlewares/create_user_middleware.py +++ b/src/bot/middlewares/create_user_middleware.py @@ -1,26 +1,25 @@ -# -*- coding: utf-8 -*- +from collections.abc import Awaitable, Callable +from typing import Any, Dict -from aiogram import types -from aiogram.dispatcher.middlewares import BaseMiddleware +from aiogram import BaseMiddleware +from aiogram.types import Message +from aiogram.types.base import TelegramObject from database import users class CreateUserMiddleware(BaseMiddleware): - async def on_pre_process_message(self, message: types.Message, data: dict): - await self._create_user_if_not_exist( - message.from_user.id, - message.from_user.first_name, - message.from_user.last_name, - message.from_user.username, - ) - async def on_pre_process_callback_query(self, call: types.CallbackQuery, data: dict): - await self._create_user_if_not_exist( - call.from_user.id, - call.from_user.first_name, - call.from_user.last_name, - call.from_user.username, - ) - - async def _create_user_if_not_exist(self, telegram_id: int, first_name: str, last_name: str, username: str): - await users.create_if_not_exist(telegram_id, first_name, last_name, username) + async def __call__( + self, + handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + message: Message, + data: Dict[str, Any], + ) -> Any: + if message.from_user is not None: + await users.create_if_not_exist( + message.from_user.id, + message.from_user.first_name, + message.from_user.last_name, + message.from_user.username, + ) + await handler(message, data) diff --git a/src/bot/middlewares/logger_middleware.py b/src/bot/middlewares/logger_middleware.py old mode 100644 new mode 100755 index 383d5e3..0cca5f2 --- a/src/bot/middlewares/logger_middleware.py +++ b/src/bot/middlewares/logger_middleware.py @@ -1,10 +1,18 @@ -# -*- coding: utf-8 -*- +from collections.abc import Awaitable, Callable +from typing import Any, Dict -from aiogram import types -from aiogram.dispatcher.middlewares import BaseMiddleware +from aiogram import BaseMiddleware, types +from aiogram.types import TelegramObject from loader import logger class UpdateLoggerMiddleware(BaseMiddleware): - async def on_process_update(self, update: types.Update, data: dict): - logger.info(update) + + async def __call__( + self, + handler: Callable[[types.TelegramObject, Dict[str, Any]], Awaitable[Any]], + event: TelegramObject, + data: Dict[str, Any], + ) -> Any: + logger.info(event) + await handler(event, data) diff --git a/src/bot/statesgroup.py b/src/bot/statesgroup.py old mode 100644 new mode 100755 index fb1355c..07232f2 --- a/src/bot/statesgroup.py +++ b/src/bot/statesgroup.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- - -from aiogram.dispatcher.filters.state import State, StatesGroup +from aiogram.fsm.state import State, StatesGroup class GetTxidFromUser(StatesGroup): diff --git a/src/bot/utils/__init__.py b/src/bot/utils/__init__.py old mode 100644 new mode 100755 diff --git a/src/bot/utils/ban_users_from_channels.py b/src/bot/utils/ban_users_from_channels.py new file mode 100755 index 0000000..a83b12c --- /dev/null +++ b/src/bot/utils/ban_users_from_channels.py @@ -0,0 +1,27 @@ +import typing +from datetime import datetime + +from data.config import private_channels +from database import users + +if typing.TYPE_CHECKING: + import aiogram + + +async def task(bot: "aiogram.Bot"): + users_records = await users.get_all() + channels = [ + await bot.get_chat(private_channels[name]["id"]) + for name in private_channels.keys() + ] + for user in users_records: + if datetime.now() < datetime.strptime(user.days_sub_end, "%Y-%m-%d %H:%M:%S"): + continue + + for channel in channels: + member = await bot.get_chat_member(channel.id, user.telegram_id) + if member.status in ["left", "creator"]: + continue + + await bot.ban_chat_member(channel.id, user.telegram_id) + await bot.unban_chat_member(channel.id, user.telegram_id) diff --git a/src/bot/utils/decrease_subscription_days.py b/src/bot/utils/decrease_subscription_days.py old mode 100644 new mode 100755 index 8bb4240..0d63eeb --- a/src/bot/utils/decrease_subscription_days.py +++ b/src/bot/utils/decrease_subscription_days.py @@ -1,23 +1,31 @@ import typing +from datetime import datetime +from data.config import SUBSCRIBE_END_NOTIFICATION_DAYS from database import users if typing.TYPE_CHECKING: import aiogram - -async def task(bot: 'aiogram.Bot'): + +async def task(bot: "aiogram.Bot"): users_records = await users.get_all() for user in users_records: - if user.days_sub_end >= 1: - await users.set_days_sub_end( - count_days=user.days_sub_end-1, - database_id=user.id, - ) + datetime_now = datetime.now() + sub_end_date = datetime.strptime(user.days_sub_end, "%Y-%m-%d %H:%M:%S") - if user.days_sub_end == 1: + days_left = (sub_end_date - datetime_now).days + 1 + print(days_left) + if days_left in SUBSCRIBE_END_NOTIFICATION_DAYS: + if days_left == 1: + await bot.send_message( + chat_id=user.telegram_id, + text="Your subscription will end soon!\n" + f"{days_left} day left until the end.", + ) + else: await bot.send_message( chat_id=user.telegram_id, - text='Your subscription will end soon!\n' - f'{user.days_sub_end} day left until the end.' + text="Your subscription will end soon!\n" + f"{days_left} days left until the end.", ) diff --git a/src/bot/utils/kick_users_from_channels.py b/src/bot/utils/kick_users_from_channels.py deleted file mode 100644 index b4a3723..0000000 --- a/src/bot/utils/kick_users_from_channels.py +++ /dev/null @@ -1,25 +0,0 @@ -import typing - -from data.config import private_channels -from database import users - -if typing.TYPE_CHECKING: - import aiogram - - -async def task(bot: 'aiogram.Bot'): - users_records = await users.get_all() - channels = [ - await bot.get_chat(private_channels[name]['id']) - for name in private_channels.keys() - ] - for user in users_records: - if user.days_sub_end >= 1: continue - - for channel in channels: - member = await bot.get_chat_member(channel.id, user.telegram_id) - if member.status in ['left', 'creator']: continue - - await bot.kick_chat_member(channel.id, user.telegram_id) - - diff --git a/src/bot/utils/subscription_checker.py b/src/bot/utils/subscription_checker.py old mode 100644 new mode 100755 index 19c795c..d070f78 --- a/src/bot/utils/subscription_checker.py +++ b/src/bot/utils/subscription_checker.py @@ -1,24 +1,53 @@ import typing +from datetime import datetime, timedelta -from data.config import NUMBER_DAYS_FROM_ONE_PAYMENT +from data.config import ( + NUMBER_DAYS_FROM_ONE_PAYMENT, + REFERAL_REWARD, + SUBSCRIBE_AMOUNT_BY_PLANS, +) from database import transactions, users +from keyboards import reply from utils import tronscan_service if typing.TYPE_CHECKING: - import aiogram + import aiogram - -async def task(bot: 'aiogram.Bot'): - records = await transactions.get_new() - for record in records: - if await tronscan_service.check_transaction_for_correct_data(record.txid): - await transactions.set_status(True, database_id=record.id) - await users.set_days_sub_end( - count_days=NUMBER_DAYS_FROM_ONE_PAYMENT, - telegram_id=record.owner_telegram_id, - ) + +async def task(bot: "aiogram.Bot"): + transactions_records = await transactions.get_new() + for transaction in transactions_records: + if await tronscan_service.check_transaction_for_correct_data( + transaction.txid, SUBSCRIBE_AMOUNT_BY_PLANS[transaction.months] + ): + await transactions.set_status(True, database_id=transaction.id) + + await users.update_subscription_date( + date=( + datetime.now() + + timedelta(days=NUMBER_DAYS_FROM_ONE_PAYMENT * transaction.months) + ).strftime("%Y-%m-%d %H:%M:%S"), + telegram_id=transaction.owner_telegram_id, + ) + await bot.send_message( + chat_id=transaction.owner_telegram_id, + text="Congratulations, you now have access to limited functionality.", + reply_markup=await reply.close_functionality(), + ) + + user = await users.get(telegram_id=transaction.owner_telegram_id) + if user is None: + continue + if user.referrer_id == 0: + continue + + referer = await users.get(database_id=user.referrer_id) + if referer is None: + continue + await users.increase_balance_by(REFERAL_REWARD, database_id=referer.id) await bot.send_message( - chat_id=record.owner_telegram_id, - text='Congratulations, you now have access to limited functionality.' + chat_id=referer.telegram_id, + text=f"Congratulations, you have received a reward of {REFERAL_REWARD} points " + + "for subscribing using your referral link.\n\n" + + f"Now your balance: {referer.balance + REFERAL_REWARD}", ) - diff --git a/src/bot/utils/tronscan_service.py b/src/bot/utils/tronscan_service.py old mode 100644 new mode 100755 index 9162769..667e256 --- a/src/bot/utils/tronscan_service.py +++ b/src/bot/utils/tronscan_service.py @@ -1,11 +1,8 @@ -# -*- coding: utf-8 -*- - import re from typing import Dict from aiohttp import ClientSession -from data.config import (SUBSCRIBE_AMOUNT_IN_USDT_TRC20, - USDT_TRC20_WALLET_ADDRESS) +from data.config import USDT_TRC20_WALLET_ADDRESS def is_valid_transaction_hash(txid: str) -> bool: @@ -14,32 +11,37 @@ def is_valid_transaction_hash(txid: str) -> bool: async def get_transaction_info(txid: str) -> Dict: - api_endpoint = 'https://apilist.tronscanapi.com/api/transaction-info' - params = {'hash': txid} - + api_endpoint = "https://apilist.tronscanapi.com/api/transaction-info" + params = {"hash": txid} + async with ClientSession() as session: async with session.get(url=api_endpoint, params=params) as response: response = await response.json() return response -async def check_transaction_for_correct_data(txid: str) -> bool: - transaction = await get_transaction_info(txid) - transaction_status = transaction.get('contractRet') - if transaction_status is None: return False - - transaction_trc20_data = transaction.get('trc20TransferInfo') - if transaction_trc20_data is None: return False +async def check_transaction_for_correct_data( + txid: str, subscription_amount: int +) -> bool: + transaction = await get_transaction_info(txid) + transaction_status = transaction.get("contractRet") + if transaction_status is None: + return False + + transaction_trc20_data = transaction.get("trc20TransferInfo") + if transaction_trc20_data is None: + return False transaction_trc20_data = transaction_trc20_data[0] - - decimals_amount = int('1' + '0' * transaction_trc20_data['decimals']) - transaction_amount = int(transaction_trc20_data['amount_str']) / decimals_amount - transaction_to_wallet = transaction_trc20_data['to_address'] - - if transaction_to_wallet == USDT_TRC20_WALLET_ADDRESS \ - and transaction_amount >= SUBSCRIBE_AMOUNT_IN_USDT_TRC20 \ - and transaction_status == 'SUCCESS': - return True - else: + + decimals_amount = int("1" + "0" * transaction_trc20_data["decimals"]) + transaction_amount = int(transaction_trc20_data["amount_str"]) / decimals_amount + transaction_to_wallet = transaction_trc20_data["to_address"] + + if ( + transaction_to_wallet == USDT_TRC20_WALLET_ADDRESS + and transaction_amount >= subscription_amount + and transaction_status == "SUCCESS" + ): + return True + else: return False - diff --git a/src/db/database_schema.sql b/src/db/database_schema.sql old mode 100644 new mode 100755 index 64e1706..3a1a6b0 --- a/src/db/database_schema.sql +++ b/src/db/database_schema.sql @@ -4,7 +4,9 @@ CREATE TABLE IF NOT EXISTS "Users" ( "first_name" TEXT, "last_name" TEXT, "username" TEXT, - "days_sub_end" INTEGER, + "days_sub_end" TEXT, + "balance" INTEGER, + "referrer_id" INTEGER, PRIMARY KEY("id" AUTOINCREMENT) ); @@ -13,6 +15,7 @@ CREATE TABLE IF NOT EXISTS "Transactions" ( "txid" TEXT, "owner_telegram_id" INTEGER, "status" BOOLEAN, + "months" INTEGER, "created_at_timestamp" INTEGER, PRIMARY KEY("id" AUTOINCREMENT) );