From 30d1c41b34df6583d2b3dab64582c642f9938dcc Mon Sep 17 00:00:00 2001 From: Nikolay Bryskin Date: Sun, 26 Feb 2023 17:22:32 +0200 Subject: [PATCH] feat(twitter): tip bot fixes --- donate4fun/api.py | 2 +- donate4fun/api_donation.py | 15 +++++++++++++++ donate4fun/twitter_bot.py | 27 +++++++++++++++++++-------- donate4fun/web.py | 7 ------- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/donate4fun/api.py b/donate4fun/api.py index a5c153f9..b69e6c85 100644 --- a/donate4fun/api.py +++ b/donate4fun/api.py @@ -229,7 +229,7 @@ async def payment_callback( db_session=Depends(get_db_session), ): """ - This callback is needed for lightning address support. + This callback is needed for lightning address support. It automatically creates a Donation. """ provider = SocialProvider.create(provider_id) receiver: SocialAccount | Donator = await provider.wrap_db(db_session).query_account(id=receiver_id) diff --git a/donate4fun/api_donation.py b/donate4fun/api_donation.py index 374436bb..015d7088 100644 --- a/donate4fun/api_donation.py +++ b/donate4fun/api_donation.py @@ -4,6 +4,7 @@ from datetime import datetime from fastapi import Request, Depends, HTTPException, APIRouter +from fastapi.responses import RedirectResponse from furl import furl from sqlalchemy import select @@ -96,6 +97,20 @@ async def get_donation(donation_id: UUID, db_session=Depends(get_donations_db)): return DonateResponse(donation=donation, payment_request=payment_request) +@router.get("/donation/{donation_id}/invoice") +async def get_invoice(donation_id: UUID, db_session=Depends(get_donations_db)): + donation: Donation = await db_session.query_donation(id=donation_id) + if donation.paid_at is None: + invoice: Invoice = await lnd.lookup_invoice(donation.r_hash) + if invoice is None or invoice.state == 'CANCELED': + logger.debug(f"Invoice {invoice} cancelled, recreating") + invoice = await lnd.create_invoice(memo=make_memo(donation), value=donation.amount) + await db_session.update_donation(donation_id=donation_id, r_hash=invoice.r_hash) + return RedirectResponse(f'lightning:{invoice.payment_request}', status_code=307) + else: + return RedirectResponse(f'/donation/{donation.id}', status_code=303) + + @router.post("/donation/{donation_id}/paid", response_model=Donation) async def donation_paid(donation_id: UUID, request: DonationPaidRequest, db=Depends(get_donations_db)): donation: Donation = await db.query_donation(id=donation_id) diff --git a/donate4fun/twitter_bot.py b/donate4fun/twitter_bot.py index d8ee0e58..b833b490 100644 --- a/donate4fun/twitter_bot.py +++ b/donate4fun/twitter_bot.py @@ -20,6 +20,7 @@ import qrcode from furl import furl from qrcode.image.styledpil import StyledPilImage +from qrcode.constants import ERROR_CORRECT_H, ERROR_CORRECT_M from lnurl.core import _url_encode as lnurl_encode from starlette.datastructures import URL @@ -329,7 +330,7 @@ async def handle_mention(self, mention: dict): # Long expiry because this will be posted in Twitter pay_req, donation = await donate(donation, db_session, expiry=3600) if pay_req: - await self.send_payreq(tweet_id, receiver_account.handle, pay_req) + await self.send_payreq(tweet_id, receiver_account.handle, donation, pay_req) elif donation.paid_at: await self.share_donation_preview(tweet_id, donation) else: @@ -344,11 +345,11 @@ async def do_not_understand(self, tweet_id: int): async def share_donation_preview(self, tweet_id: int, donation: Donation): await self.send_tweet(reply_to=tweet_id, text=make_absolute_uri(f'/donation/{donation.id}')) - async def send_payreq(self, tweet_id: int, handle: TwitterHandle, pay_req: PaymentRequest): - qrcode: bytes = make_qr_code(pay_req) + async def send_payreq(self, tweet_id: int, handle: TwitterHandle, donation: Donation, pay_req: PaymentRequest): + qrcode: bytes = make_qr_code(pay_req, ERROR_CORRECT_M) media_id: int = await self.client.upload_media(qrcode, 'image/png', category='tweet_image') - url: str = make_absolute_uri(f'/lnurlp/twitter/{handle}') - await self.send_tweet(text=f"Invoice: {url}", reply_to=tweet_id, media_id=media_id) + invoice_url = make_absolute_uri(f'/api/v1/donation/{donation.id}/invoice') + await self.send_tweet(text=f"Invoice: {invoice_url}", reply_to=tweet_id, media_id=media_id) async def fetch_mentions(self, handle: TwitterHandle) -> AsyncIterator[dict]: logger.info("fetching new mentions for @%s", handle) @@ -370,7 +371,11 @@ async def fetch_mentions(self, handle: TwitterHandle) -> AsyncIterator[dict]: 'expansions': 'author_id,referenced_tweets.id,in_reply_to_user_id,referenced_tweets.id.author_id', } async with self.apponly_client.stream('GET', '/tweets/search/stream', params=params, timeout=3600) as response: - response.raise_for_status() + try: + response.raise_for_status() + except httpx.HTTPStatusError: + await response.aread() + raise async for chunk in response.aiter_text(): chunk = chunk.strip() if chunk: @@ -416,8 +421,14 @@ async def create_withdrawal(db_session, twitter_account): return lnurl_encode(str(withdraw_url)) -def make_qr_code(data: str) -> bytes: - qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H) +def make_qr_code(data: str, error_correction: int = ERROR_CORRECT_H) -> bytes: + """ + L - 7% + M - 15% + Q - 25% + H - 30% + """ + qr = qrcode.QRCode(error_correction=error_correction) qr.add_data(data) image: StyledPilImage = qr.make_image() image_data = io.BytesIO() diff --git a/donate4fun/web.py b/donate4fun/web.py index df427f8c..10a82df8 100644 --- a/donate4fun/web.py +++ b/donate4fun/web.py @@ -4,7 +4,6 @@ from xml.etree import ElementTree as ET import httpx -from bech32 import bech32_encode from mako.lookup import TemplateLookup from fastapi import Request, Response, FastAPI, Depends from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse @@ -164,12 +163,6 @@ async def lnurlp_redirect(provider: str, username: str): return RedirectResponse(url, status_code=302) -@app.get('/lightning/{provider}/{username}') -async def lightning_redirect(provider: str, username: str): - encoded_url = bech32_encode(make_absolute_uri(f'/.well-known/lnurlp/{provider}/{username}')) - return RedirectResponse(f'lightning:{encoded_url}', status_code=302) - - @app.get("/.well-known/openid-configuration", response_class=JSONResponse) async def openid_configuration(): return dict(