From 839655b16304613bef8ca654cd4c2317b884a731 Mon Sep 17 00:00:00 2001 From: user Date: Fri, 5 Apr 2024 21:05:45 +0300 Subject: [PATCH] license_service --- app/client_bot/routers/content.py | 54 ++++++++++++++++++ app/core/background/license_service.py | 55 ++++++++++++++++++- app/core/models/_telegram/templates/player.py | 25 +++++++-- app/core/models/content/indexation_mixins.py | 47 +++++++++++++++- 4 files changed, 170 insertions(+), 11 deletions(-) diff --git a/app/client_bot/routers/content.py b/app/client_bot/routers/content.py index 20789b0..cc2d190 100644 --- a/app/client_bot/routers/content.py +++ b/app/client_bot/routers/content.py @@ -8,6 +8,7 @@ from app.core._keyboards import get_inline_keyboard from app.core.models.node_storage import StoredContent import json from app.core.logger import make_log +from app.core.models.content.user_content import UserAction from app.client_bot.routers.home import router as home_router from app.client_bot.routers.tonconnect import router as tonconnect_router @@ -15,10 +16,63 @@ from app.core._config import CLIENT_TELEGRAM_BOT_USERNAME from app.core.logger import logger from app.core.content.content_id import ContentId import base58 +from datetime import datetime, timedelta +import asyncio +from app.core._blockchain.ton.connect import TonConnect, wallet_obj_by_name + router = Router() +async def t_callback_purchase_node_content(query: types.CallbackQuery, memory=None, user=None, db_session=None, chat_wrap=None, **extra): + content_oid = int(query.data.split('_')[1]) + make_log("OwnedContent", f"{user} Try to purchase content: {content_oid}", level='info') + content = db_session.query(StoredContent).filter_by(id=content_oid).first() + if not content: + return await query.answer(user.translated('error_contentNotFound'), show_alert=True) + + license_price = content.meta.get('license', {}).get('listen', {}).get('price') + license_price_num = int(license_price) + if license_price_num < 1: + return await query.answer(user.translated('error_contentPrice'), show_alert=True) + + ton_connect, ton_connection = TonConnect.by_user(db_session, user, callback_fn=()) + await ton_connect.restore_connection() + assert ton_connect.connected, "No connected wallet" + + user_wallet_address = user.wallet_address(db_session) + + memory._app.add_task(ton_connect._sdk_client.send_transaction({ + 'valid_until': int(datetime.now().timestamp() + 300), + 'messages': [ + { + 'address': content.meta['item_address'], + 'amount': license_price + } + ] + })) + + new_action = UserAction( + type='purchase', + user_id=user.id, + content_id=content.id, + telegram_message_id=query.message.message_id, + from_address=user_wallet_address, + to_address=content.meta['item_address'], + status='requested', + meta={ + 'confirmation_url': wallet_obj_by_name(ton_connection.wallet_key.split('==')[0])['universal_url'] + }, + created_at=datetime.now() + ) + db_session.add(new_action) + db_session.commit() + await user.send_content(db_session, content, message_id=query.message.message_id) + + +router.callback_query.register(t_callback_purchase_node_content, F.data.startswith('PC_')) + + async def t_inline_query_node_content(query: types.InlineQuery, memory=None, user=None, db_session=None, chat_wrap=None, **extra): make_log("OwnedContent", f"Inline query: {query.query}", level='info') try: diff --git a/app/core/background/license_service.py b/app/core/background/license_service.py index a971d43..a0580ab 100644 --- a/app/core/background/license_service.py +++ b/app/core/background/license_service.py @@ -1,8 +1,9 @@ import asyncio from base64 import b64decode -from datetime import datetime +from datetime import datetime, timedelta from base58 import b58encode +from sqlalchemy import and_ from tonsdk.boc import Cell from tonsdk.utils import Address @@ -11,11 +12,14 @@ from app.core._blockchain.ton.toncenter import toncenter from app.core._utils.send_status import send_status from app.core.logger import make_log from app.core.models.node_storage import StoredContent +from app.core.models.content.user_content import UserContent, UserAction from app.core._utils.resolve_content import resolve_content from app.core.models.wallet_connection import WalletConnection from app.core._keyboards import get_inline_keyboard from app.core.models._telegram import Wrapped_CBotChat from app.core.storage import db_session +from app.core._config import CLIENT_TELEGRAM_API_KEY +from app.core.models.user import User import os import traceback @@ -23,7 +27,54 @@ import traceback async def license_index_loop(memory, platform_found: bool, seqno: int) -> [bool, int]: make_log("LicenseIndex", "Service running", level="debug") with db_session() as session: - pass + for user in session.query(User).filter( + User.last_use > datetime.now() - timedelta(minutes=10) + ).all(): + try: + await user.scan_owned_user_content(session) + except BaseException as e: + make_log("LicenseIndex", f"Error: {e}" + '\n' + traceback.format_exc(), level="error") + + for content in session.query(UserContent).filter( + and_( + UserContent.startswith('nft/'), + UserContent.updated < (datetime.now() - timedelta(minutes=15)), + ) + ): + try: + await content.sync_with_chain(session) + except BaseException as e: + make_log("LicenseIndex", f"Error: {e}" + '\n' + traceback.format_exc(), level="error") + + for action in session.query(UserAction).filter( + and_( + UserAction.type == 'purchase', + # UserAction.updated < (datetime.now() - timedelta(minutes=5)), + UserAction.status == 'requested', + ) + ): + try: + user = session.query(User).filter_by(id=action.user_id).first() + chat_wrap = Wrapped_CBotChat(CLIENT_TELEGRAM_API_KEY, chat_id=user.telegram_id, db_session=session, user=user) + content = session.query(StoredContent).filter_by(id=action.content_id).first() + + if (datetime.now() - action.updated) > timedelta(minutes=5): + if action.telegram_message_id: + await chat_wrap.delete_message(action.telegram_message_id) + + make_log("LicenseIndex", f"Action timeout: {action.id}", level="info") + action.status = 'canceled' + else: + user_wallet_address = user.wallet_address(session) + user_content = session.query(UserContent).filter_by(content_id=action.content_id, status='active', owner_address=user_wallet_address).first() + if user_content: + make_log("LicenseIndex", f"User already has content: {user_content.content_id}", level="info") + action.status = 'success' + + session.commit() + await chat_wrap.send_content(session, content) + except BaseException as e: + make_log("LicenseIndex", f"Error: {e}" + '\n' + traceback.format_exc(), level="error") return platform_found, seqno diff --git a/app/core/models/_telegram/templates/player.py b/app/core/models/_telegram/templates/player.py index b0b5b79..c5af14e 100644 --- a/app/core/models/_telegram/templates/player.py +++ b/app/core/models/_telegram/templates/player.py @@ -1,5 +1,5 @@ from app.core.models.node_storage import StoredContent -from app.core.models.content.user_content import UserContent +from app.core.models.content.user_content import UserContent, UserAction from app.core.logger import make_log from app.core._utils.tg_process_template import tg_process_template from app.core._config import PROJECT_HOST, CLIENT_TELEGRAM_BOT_USERNAME @@ -96,12 +96,25 @@ class PlayerTemplates: or bool(self.db_session.query(UserContent).filter_by(owner_address=user_wallet_address, status='active', content_id=content.id).first()) ) if not have_access: - inline_keyboard_array.append([{ - 'text': self.user.translated('buyTrackListenLicense_button').format(price=str(round(0.15 , 3))), - 'callback_data': f'PC_{content.id}' - }]) template_kwargs['audio'] = URLInputFile(local_content_url + '?seconds_limit=30') - text = self.user.translated('p_playerContext_preview') + purchase_action = self.db_session.query(UserAction).filter_by( + type='purchase', + from_address=user_wallet_address, + content_id=content.id, + status='requested' + ).first() + if purchase_action: + inline_keyboard_array.append([{ + 'text': self.user.translated('gotoWallet_button'), + 'url': purchase_action.meta['confirmation_url'] + }]) + text = self.user.translated('p_playerContext_purchaseRequested') + else: + inline_keyboard_array.append([{ + 'text': self.user.translated('buyTrackListenLicense_button').format(price=str(round(0.15, 3))), + 'callback_data': f'PC_{content.id}' + }]) + text = self.user.translated('p_playerContext_preview') make_log("TG-Player", f"Send content {content_type} ({content_encoding}) to chat {self._chat_id}. {cd_log}") for kmsg in self.db_session.query(KnownTelegramMessage).filter_by( diff --git a/app/core/models/content/indexation_mixins.py b/app/core/models/content/indexation_mixins.py index 892553b..0a9404d 100644 --- a/app/core/models/content/indexation_mixins.py +++ b/app/core/models/content/indexation_mixins.py @@ -1,3 +1,10 @@ +import traceback + +import base58 +from sqlalchemy import and_ + +from app.core.logger import make_log +from app.core.models import StoredContent from app.core.models.wallet_connection import WalletConnection from app.core._blockchain.ton.toncenter import toncenter from tonsdk.boc import Cell @@ -35,10 +42,44 @@ class NodeStorageIndexationMixin: pass # async def fetch_onchain_metadata(self): - class UserContentIndexationMixin: - pass - + async def sync_with_chain(self, db_session): + errored = False + cc_indexator_result = await toncenter.run_get_method(self.onchain_address, 'indexator_data') + if cc_indexator_result.get('exit_code', -1) != 0: + errored = True + + if not errored: + try: + cc_indexator_data = unpack_item_indexator_data(cc_indexator_result) + assert cc_indexator_data['type'] == 1, "Type is not a content" + assert cc_indexator_data['address'] == self.onchain_address, "Address is not equal" + values_slice = cc_indexator_data['values'].begin_parse() + content_hash_b58 = base58.b58encode(bytes.fromhex(values_slice.read_uint(256).hex()[2:])).decode() + stored_content = db_session.query(StoredContent).filter( + and_( + StoredContent.type == 'onchain/content', + StoredContent.hash == content_hash_b58, + + ) + ).first() + trusted_cop_address_result = await toncenter.run_get_method(stored_content.meta['item_address'], 'get_nft_address_by_index', [['num', cc_indexator_data['index']]]) + assert trusted_cop_address_result.get('exit_code', -1) == 0, "Trusted cop address error" + trusted_cop_address = Cell.one_from_boc(b64decode(trusted_cop_address_result['stack'][0][1]['bytes'])).begin_parse().read_msg_addr().to_string(1, 1, 1) + make_log("UserContent", f"Trusted cop address: {trusted_cop_address} / Contract address: {trusted_cop_address}", level="info") + assert trusted_cop_address == cc_indexator_data['address'], "Trusted cop address is not equal" + self.owner_address = cc_indexator_data['owner_address'] + self.type = 'nft/listen' + self.content_id = stored_content.id + db_session.commit() + except BaseException as e: + errored = True + make_log("UserContent", f"Error: {e}" + '\n' + traceback.format_exc(), level="error") + + if errored is True: + self.type = 'nft/unknown' + self.content_id = None + db_session.commit()