diff --git a/app/client_bot/routers/index.py b/app/client_bot/routers/index.py index be80bb8..607f5fa 100644 --- a/app/client_bot/routers/index.py +++ b/app/client_bot/routers/index.py @@ -7,6 +7,7 @@ from aiogram import types, Router from app.client_bot.routers.home import router as home_router from app.client_bot.routers.tonconnect import router as tonconnect_router from app.client_bot.routers.content import router as content_router +from app.client_bot.routers.stars import router as stars_router from app.core.logger import logger @@ -14,6 +15,7 @@ main_router = Router() main_router.include_routers(home_router) main_router.include_routers(tonconnect_router) main_router.include_routers(content_router) +main_router.include_routers(stars_router) closing_router = Router() diff --git a/app/client_bot/routers/stars.py b/app/client_bot/routers/stars.py new file mode 100644 index 0000000..b25b50d --- /dev/null +++ b/app/client_bot/routers/stars.py @@ -0,0 +1,25 @@ +from aiogram import types, Router, F +from app.core.logger import make_log +from app.core.models import StarsInvoice + +router = Router() + + +async def t_pre_checkout_query_stars_processing(pre_checkout_query: types.PreCheckoutQuery, user=None, db_session=None, + chat_wrap=None, **extra): + invoice_id = pre_checkout_query.invoice_payload + + existing_invoice = db_session.query(StarsInvoice).filter( + StarsInvoice.external_id == invoice_id + ).first() + if not existing_invoice: + return await pre_checkout_query.answer(ok=False, error_message="Invoice not found") + + if existing_invoice.paid: + return await pre_checkout_query.answer(ok=False, error_message="Invoice already paid") + + return await pre_checkout_query.answer(ok=True) + + + +router.pre_checkout_query.register(t_pre_checkout_query_stars_processing) \ No newline at end of file diff --git a/app/core/background/indexer_service.py b/app/core/background/indexer_service.py index 9095f5d..dd581db 100644 --- a/app/core/background/indexer_service.py +++ b/app/core/background/indexer_service.py @@ -11,7 +11,7 @@ from app.core._blockchain.ton.platform import platform 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 import UserContent +from app.core.models import UserContent, KnownTelegramMessage from app.core.models.node_storage import StoredContent from app.core._utils.resolve_content import resolve_content from app.core.models.wallet_connection import WalletConnection @@ -212,6 +212,16 @@ async def indexer_loop(memory, platform_found: bool, seqno: int) -> [bool, int]: }] ]) ) + + user_uploader_wrapper = Wrapped_CBotChat(memory._telegram_bot, chat_id=user.telegram_id, user=user, db_session=session) + for hint_message in session.query(KnownTelegramMessage).filter( + and_( + KnownTelegramMessage.user_id == user.id, + KnownTelegramMessage.message_type == 'hint', + KnownTelegramMessage.meta.contains({'encrypted_content_hash': encrypted_stored_content.hash}) + ) + ): + await user_uploader_wrapper.delete_message(hint_message.message_id) elif encrypted_stored_content.type.startswith('onchain') and encrypted_stored_content.onchain_index == item_index: encrypted_stored_content.type = "onchain/content" + ("_unknown" if (encrypted_stored_content.key_id is None) else "") encrypted_stored_content.owner_address = item_owner_address.to_string(1, 1, 1) diff --git a/app/core/background/license_service.py b/app/core/background/license_service.py index 2b27ba6..1389f56 100644 --- a/app/core/background/license_service.py +++ b/app/core/background/license_service.py @@ -20,6 +20,7 @@ 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 +from app.core.models import StarsInvoice import os import traceback @@ -27,6 +28,51 @@ 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: + async def check_telegram_stars_transactions(): + # Проверка звездных telegram транзакций, обновление paid + offset = {'desc': 'Статичное число заранее известного количества транзакций, которое даже не знает наш бот', 'value': 1}['value'] + \ + session.query(StarsInvoice).count() + limit = 100 + while True: + make_log("StarsProcessing", f"Star payments: offset={offset}, limit={limit}", level="DEBUG") + star_payments = (await memory._client_telegram_bot.get_star_transactions(offset, limit)).transactions + if not star_payments: + make_log("StarsProcessing", "No more star payments", level="DEBUG") + return + + for star_payment in star_payments: + if star_payment.receiver: + continue + + try: + existing_invoice = session.query(StarsInvoice).filter( + StarsInvoice.external_id == star_payment.source.invoice_payload + ).first() + if not existing_invoice: + continue + + if star_payment.amount == existing_invoice.amount: + if not existing_invoice.paid: + existing_invoice.paid = True + session.commit() + + licensed_content = session.query(StoredContent).filter(StoredContent.hash == existing_invoice.content_hash).first() + user = session.query(User).filter(User.id == existing_invoice.user_id).first() + + await (Wrapped_CBotChat(memory._client_telegram_bot, chat_id=user.telegram_id, user=user, db_session=session)).send_content( + session, licensed_content + ) + except BaseException as e: + make_log("StarsProcessing", f"Local error: {e}" + '\n' + traceback.format_exc(), level="error") + + offset += limit + + try: + await check_telegram_stars_transactions() + except BaseException as e: + make_log("StarsProcessing", f"Error: {e}" + '\n' + traceback.format_exc(), level="error") + + # Проверка кошельков пользователей на появление новых NFT, добавление их в базу как неопознанные for user in session.query(User).filter( User.last_use > datetime.now() - timedelta(minutes=10) ).all(): @@ -47,6 +93,7 @@ async def license_index_loop(memory, platform_found: bool, seqno: int) -> [bool, except BaseException as e: make_log("LicenseIndex", f"Error: {e}" + '\n' + traceback.format_exc(), level="error") + # Проверка NFT на актуальность данных, в том числе уже проверенные process_content = session.query(UserContent).filter( and_( UserContent.type.startswith('nft/'), diff --git a/app/core/models/_telegram/templates/player.py b/app/core/models/_telegram/templates/player.py index ef4c863..31e310c 100644 --- a/app/core/models/_telegram/templates/player.py +++ b/app/core/models/_telegram/templates/player.py @@ -1,3 +1,4 @@ +from sqlalchemy import and_ from app.core.models.node_storage import StoredContent from app.core.models.content.user_content import UserContent, UserAction from app.core.logger import make_log @@ -7,6 +8,9 @@ from app.core._keyboards import get_inline_keyboard from app.core.models.messages import KnownTelegramMessage from aiogram.types import URLInputFile, Message import json +import urllib + +from app.core.models.transaction import StarsInvoice class PlayerTemplates: @@ -55,8 +59,25 @@ class PlayerTemplates: local_content.meta['cover_cid'] = cover_content.cid.serialize_v2() if cover_content else None local_content_cid = local_content.cid - local_content_cid.content_type = 'audio/mpeg' - local_content_url = f"{PROJECT_HOST}/api/v1/storage/{local_content_cid.serialize_v2(include_accept_type=True)}" + local_content_url = f"{PROJECT_HOST}/api/v1.5/storage/{local_content_cid.serialize_v2()}" + converted_content = content.meta.get('converted_content') + if not converted_content: + r = await tg_process_template( + self, self.user.translated('p_playerContext_contentNotReady'), + message_id=message_id, + message_type='common' + ) + return r + + content_share_link = { + 'text': self.user.translated('p_shareLinkContext').format(title=content_metadata_json.get('name', "")), + 'url': f"https://t.me/{CLIENT_TELEGRAM_BOT_USERNAME}?start=C{content.cid.serialize_v2()}" + } + + preview_content = db_session.query(StoredContent).filter( + StoredContent.hash == converted_content['low_preview'] + ).first() + local_content_preview_url = preview_content.web_url if content_type == 'audio': audio_title = content_metadata_json.get('name', "").split(' - ') if len(audio_title) > 1: @@ -65,15 +86,21 @@ class PlayerTemplates: template_kwargs['title'] = audio_title[0].strip() template_kwargs['protect_content'] = True - template_kwargs['audio'] = URLInputFile(local_content_url) + template_kwargs['audio'] = URLInputFile(local_content_preview_url) if cover_content: template_kwargs['thumbnail'] = URLInputFile(cover_content.web_url) if self.bot_id == 1: - inline_keyboard_array.append([{ - 'text': self.user.translated('shareTrack_button'), - 'switch_inline_query': f"C{content.cid.serialize_v2()}", - }]) + inline_keyboard_array.append([ + { + 'text': self.user.translated('shareTrack_button'), + 'switch_inline_query': f"C{content.cid.serialize_v2()}", + }, + { + 'text': self.user.translated('shareLink_button'), + 'url': f"https://t.me/share/url?text={urllib.parse.quote(content_share_link['text'])}&url={urllib.parse.quote(content_share_link['url'])}" + } + ]) inline_keyboard_array.append([{ 'text': self.user.translated('openTrackInApp_button'), 'url': f"https://t.me/{CLIENT_TELEGRAM_BOT_USERNAME}/content?startapp={content.cid.serialize_v2()}" @@ -90,7 +117,7 @@ class PlayerTemplates: elif content_type == 'video': # Processing video video_title = content_metadata_json.get('name', "") - template_kwargs['video'] = URLInputFile(local_content_url) + template_kwargs['video'] = URLInputFile(local_content_preview_url) template_kwargs['protect_content'] = True if cover_content: @@ -99,10 +126,16 @@ class PlayerTemplates: if self.bot_id == 1: # Buttons for sharing and opening in app - inline_keyboard_array.append([{ - 'text': self.user.translated('shareVideo_button'), - 'switch_inline_query': f"C{content.cid.serialize_v2()}", - }]) + inline_keyboard_array.append([ + { + 'text': self.user.translated('shareVideo_button'), + 'switch_inline_query': f"C{content.cid.serialize_v2()}", + }, + { + 'text': self.user.translated('shareLink_button'), + 'url': f"https://t.me/share/url?text={urllib.parse.quote(content_share_link['text'])}&url={urllib.parse.quote(content_share_link['url'])}" + } + ]) inline_keyboard_array.append([{ 'text': self.user.translated('openTrackInApp_button'), 'url': f"https://t.me/{CLIENT_TELEGRAM_BOT_USERNAME}/content?startapp={content.cid.serialize_v2()}" @@ -134,14 +167,24 @@ class PlayerTemplates: have_access = ( (content.owner_address == user_wallet_address) or bool(self.db_session.query(UserContent).filter_by(owner_address=user_wallet_address, status='active', content_id=content.id).first()) + or bool(self.db_session.query(UserContent).filter( + and_( + StarsInvoice.user_id == self.user, + StarsInvoice.content_hash == content['encrypted_content'].hash, + StarsInvoice.paid == True + ) + )) ) - if not have_access: + if have_access: + full_content = self.db_session.query(StoredContent).filter_by( + hash=content.meta.get('converted_content', {}).get('low') # TODO: support high quality + ).first() if content_type == 'audio': # Restrict audio to 30 seconds if user does not have access - template_kwargs['audio'] = URLInputFile(local_content_url + '?seconds_limit=30') + template_kwargs['audio'] = URLInputFile(full_content.web_url) elif content_type == 'video': # Restrict video to 30 seconds if user does not have access - template_kwargs['video'] = URLInputFile(local_content_url + '?seconds_limit=30') + template_kwargs['video'] = URLInputFile(full_content.web_url) 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/locale/en/LC_MESSAGES/sanic_telegram_bot.mo b/locale/en/LC_MESSAGES/sanic_telegram_bot.mo index 3e68790..0d09927 100644 Binary files a/locale/en/LC_MESSAGES/sanic_telegram_bot.mo and b/locale/en/LC_MESSAGES/sanic_telegram_bot.mo differ diff --git a/locale/en/LC_MESSAGES/sanic_telegram_bot.po b/locale/en/LC_MESSAGES/sanic_telegram_bot.po index 6bc61c8..c1acc9f 100644 --- a/locale/en/LC_MESSAGES/sanic_telegram_bot.po +++ b/locale/en/LC_MESSAGES/sanic_telegram_bot.po @@ -21,21 +21,21 @@ msgstr "" #: app/client_bot/routers/home.py:15 app/client_bot/routers/home.py:17 msgid "home_menu" msgstr "" -"Hi, {name}!\n" +"Hi, {name}! 😊\n" "\n" -"Here you can upload your content to the blockchain and manage it.\n" +"Here you can upload your content to the blockchain and manage it 🚀.\n" "\n" -"You logged as {wallet_address}" +"You're logged in as {wallet_address}" #: app/bot/routers/home.py:20 app/client_bot/routers/home.py:20 #: app/client_bot/routers/home.py:22 msgid "ownedContent_button" -msgstr "📊 My content" +msgstr "📊 My Content" #: app/bot/routers/home.py:24 app/client_bot/routers/home.py:24 #: app/client_bot/routers/home.py:26 msgid "disconnectWallet_button" -msgstr "🔌 Disconnect wallet" +msgstr "🔌 Disconnect Wallet" #: app/bot/routers/home.py:35 app/client_bot/routers/home.py:35 #: app/client_bot/routers/home.py:37 @@ -43,25 +43,25 @@ msgid "connectWalletsList_menu" msgstr "" "/ Welcome to MY [🔴]\n" "\n" -"Please select the wallet you want to connect to the bot:" +"Please select the wallet you want to connect to the bot 😊:" #: app/bot/routers/index.py:23 app/bot/routers/index.py:22 #: app/client_bot/routers/index.py:18 app/client_bot/routers/index.py:23 msgid "error_unknownCommand" -msgstr "Unknown command, please try again or press /start" +msgstr "❌ Unknown command, please try again or press /start" #: app/bot/routers/content.py:16 app/bot/routers/content.py:12 #: app/bot/routers/content.py:21 msgid "ownedContent_menu" msgstr "" -"📊 My content\n" +"📊 My Content\n" "\n" -"Here you can see the list of your content." +"Here you can view the list of your content 📋." #: app/bot/routers/content.py:21 app/bot/routers/content.py:17 #: app/bot/routers/content.py:47 msgid "webApp_uploadContent_button" -msgstr "📤 Upload content" +msgstr "📤 Upload Content" #: app/bot/routers/content.py:27 app/bot/routers/content.py:23 #: app/bot/routers/content.py:53 app/bot/routers/content.py:68 @@ -88,7 +88,7 @@ msgstr "" #: app/core/models/_telegram/templates/player.py:108 #: app/api/routes/_blockchain.py:145 msgid "gotoWallet_button" -msgstr "Open wallet" +msgstr "🔓 Open Wallet" #: app/api/routes/_blockchain.py:150 app/bot/routers/tonconnect.py:90 #: app/core/background/indexer_service.py:141 app/bot/routers/tonconnect.py:92 @@ -99,78 +99,82 @@ msgstr "◀️ Back" #: app/bot/routers/tonconnect.py:86 app/client_bot/routers/tonconnect.py:88 msgid "tonconnectOpenWallet_button" -msgstr "[Connect wallet]" +msgstr "[Connect Wallet]" #: app/core/background/indexer_service.py:134 #: app/core/background/indexer_service.py:149 msgid "p_contentWasIndexed" msgstr "" -"🎉 Your new content was uploaded successfully!\n" +"🎉 Your new content has been uploaded successfully!\n" "\n" -"🔗 {item_address} on " -"the blockchain\n" +"🔗 {item_address} on the blockchain\n" "\n" -"Now you can manage it in My content section" +"Now you can manage it in the My Content section" #: app/core/models/_telegram/templates/player.py:74 #: app/client_bot/routers/content.py:129 msgid "shareTrack_button" -msgstr "Share track" +msgstr "🎶 Share Track" msgid "shareVideo_button" -msgstr "Share video" +msgstr "🎬 Share Video" #: app/core/models/_telegram/templates/player.py:79 msgid "viewTrackAsClient_button" -msgstr "Open in player" +msgstr "▶️ Open in Player" #: app/core/models/_telegram/templates/player.py:86 msgid "p_playerContext_unsupportedContent" -msgstr "⚠️ Unsupported content" +msgstr "⚠️ Unsupported Content" #: app/core/models/_telegram/templates/player.py:111 msgid "p_playerContext_purchaseRequested" -msgstr "Confirm the purchase in your wallet. After that, you will be able to " -"listen to the full track version.\n\n" -"This can take up to few minutes" +msgstr "💳 Please confirm the purchase in your wallet. After that, you'll be able to listen to the full version of the track.\n\n" +"This may take a few minutes ⏳" #: app/core/models/_telegram/templates/player.py:114 msgid "buyTrackListenLicense_button" -msgstr "Buy license ({price} TON)" +msgstr "💶 Buy License ({price} TON)" #: app/core/models/_telegram/templates/player.py:117 msgid "p_playerContext_preview" -msgstr "It's a preview version of the track. To listen to the full version, " -"you need to buy a license." +msgstr "This is a preview version of the track. To listen to the full version, you need to buy a license 🎧." #: app/client_bot/routers/content.py:32 msgid "error_contentNotFound" -msgstr "Error: content not found" +msgstr "❗ Error: Content not found" #: app/client_bot/routers/content.py:37 msgid "error_contentPrice" -msgstr "Error: content price is not set" +msgstr "❗ Error: Content price is not set" #: app/client_bot/routers/content.py:133 msgid "viewTrack_button" -msgstr "Open in mini-app" +msgstr "▶️ Open in Mini-App" #: app/client_bot/routers/content.py:138 msgid "cancelPurchase_button" -msgstr "Cancel purchase" +msgstr "❌ Cancel Purchase" msgid "openContractPage_button" -msgstr "Open smartcontract" +msgstr "🔗 Open Smart Contract" msgid "noWalletConnected" -msgstr "No wallet connected" +msgstr "❗ No wallet connected" msgid "openTrackInApp_button" -msgstr "Open in app" +msgstr "📱 Open in App" msgid "p_licenseWasBought" -msgstr "💶 {username} just purchased a license for {content_title}. " -"👏 Your content is gaining more appreciation! Keep creating and inspiring! 🚀" +msgstr "" +"💶 {username} just purchased a license for {content_title}. 👏\n" +"Your content is gaining popularity! Keep creating and inspiring 🚀." msgid "p_uploadContentTxRequested" -msgstr "Transaction for upload {title} requested. Confirm that and wait notification of transaction result. Convert content may be 20 min" +msgstr "⏳ Transaction for uploading {title} has been requested. Please confirm the transaction and wait for the notification. Content conversion may take up to 20 minutes." + +msgid "shareLink_button" +msgstr "🔗 Share Link" + +msgid "p_shareLinkContext" +msgstr "🎉 Enjoy {title} on MY!"