From 758defee997585a9ce26eb54ec5b2617fcd0084f Mon Sep 17 00:00:00 2001 From: user Date: Fri, 28 Feb 2025 13:27:37 +0300 Subject: [PATCH] stars --- app/client_bot/routers/index.py | 2 + app/client_bot/routers/stars.py | 25 ++++++ app/core/background/indexer_service.py | 12 ++- app/core/background/license_service.py | 47 +++++++++++ app/core/models/_telegram/templates/player.py | 73 +++++++++++++---- locale/en/LC_MESSAGES/sanic_telegram_bot.mo | Bin 3285 -> 3566 bytes locale/en/LC_MESSAGES/sanic_telegram_bot.po | 76 +++++++++--------- 7 files changed, 183 insertions(+), 52 deletions(-) create mode 100644 app/client_bot/routers/stars.py 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 3e687906e5f36c2b9ed3f992571ab1b37b1fb47b..0d099272c99913d5854c1e70ad6bde53ac1816e7 100644 GIT binary patch delta 1717 zcmZvbU1(fI6vroR(rkB=*j8}qX1>;rlI2*`$qK)(MRxD5bI;3u?HXQU=-{Di{NJPRgfRd#`~Xvo1o6O zAZPOH8W;nA2l;(RyAaQV1abgpKvrx9+zws^9|doKY&Vs_gn!YCk7;lx$eCROIe;HQ zzW5t>8~8Uk1a?Bd>);XaQ}EuT5EI}J;2iiQqGM%#0=vO~;&pe25Mx;H20sEXfHgM! zSZBl0(=o@v0qmFJ^}8UK<_gFGeg$&IOCSWXG6U8r69NfTab>UyEIF5n$K9B$9BkHX zW=PC73mfHuk_o6}>&7MFf&N<=@N1h035%_mtQHS03kS%9mFvRfo?#W51Jx2p+?GgW z=f7L^WlLAd@#O)f%lkZS~IqA|!bZyOv94*hib9R?lo|~DXr&N&hw3azTdQl+~G@dy<$=3N$F@~qOZmQ9Q5dv4Ts|BWS--_)G#+X;rPZz=Q}xrgXn7G zKxx`jsqn^G8m>0uZ>%%f!CPCI{_J2fIV9n{>V=eZG)*dbicCpvAy);4yaJ#2?B7Ue z0Gn?{pC@_}B`2V)lsZ~UTu9+Q#fGDkYbUpJMz`KTl%k`w(nYFxmC$v3WvbEo0qkvvVqN4JUk>}zqGAgX6m1ZZJHAm-BX2s3r+f>kKs-SWX+e$ZM;u1_sKVbVSxrG%n9t}LyOfL<| z&>o}wf5HDim$5##5U&wo&Td?ZH;$;h^h9*LBlAq7>yL~2+Y2mi0f3P|KHtV R6!XEj!#fCol-}9iDAMY-cXRKa7W92lxo&-=Bc%!B^l0Fuyz_fqcFb>;lKYb)W>_ z0%t+iIRUN$&t~ThaTyC1S^!z#A-EQNnq3gDn)~+lOt=X0{q0~cI12WG3hV|?fCSEi zZ-aM0PT=n*pM$LbFW4g**sKtuh=T#}O>j5Z10Du9z>OH>%zs@e#76Ku_y)KLehuCO zIdD%d(-{P}V80Li2%G`={iPk!3FSV z_@}^63qqU*FM{mEHauqT2ggAJ9tZD%_!GNUXFLcFVt)i|aK&e^SOZ=KdA|U%MR!3M zkPdDQi*TjfKbRs=KssPR`i}d}4s~J@W^1q(4>+DW&F=ERcd|F_4GIwWp3=S|W;)nH zv_5r$lWPs`DSwEqu?dUKnEA!m%O12PkJ~2}KUs0Jt#2f*ldnBxB1P}dg%-;nY5c(0 z2VztjPx%xk#oV_;L*nK9*#mLKWtFBA?3G5NvNlqx6Z^L>r6pef2~97B~w`Y5VO>o|gy zp39E3AsL29Dmg`8)?4ATeQ9#||D)2t3i z&<~B%CkYzJ&XQ5JmN=4&lG>^>xu$;7RhZpI>8)1vI-)75jW$zM4G(^d90mXJ}U02=Cw%0*+5Z zo0E5PMo}D+3#fwJs@lX(MO>wLcHzoDfBeR&PnakU!@$D9R`z@&DX#vZF>a-a;3K5b zDGMz>kY$>4+WBa`Ro}-d!oz+V4NOJbD!h`8q$$GyopxAPw3pVz?jhMFJ*%SFcj-%H z?^>~`D(f_{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!"