From 435787ad9304ace93ea180bc4490b32de8df286e Mon Sep 17 00:00:00 2001 From: Fitti Date: Sat, 3 Jul 2021 18:01:04 +0200 Subject: [PATCH] Cleanup, Comments, Docstrings --- lnbits/extensions/twitchalerts/README.md | 2 +- lnbits/extensions/twitchalerts/__init__.py | 5 ++- lnbits/extensions/twitchalerts/crud.py | 38 +++++++++++++++++++++ lnbits/extensions/twitchalerts/models.py | 35 +++++++++++-------- lnbits/extensions/twitchalerts/views.py | 2 ++ lnbits/extensions/twitchalerts/views_api.py | 29 ++++++++++++---- 6 files changed, 89 insertions(+), 22 deletions(-) diff --git a/lnbits/extensions/twitchalerts/README.md b/lnbits/extensions/twitchalerts/README.md index 543fc337a..5bc1369d6 100644 --- a/lnbits/extensions/twitchalerts/README.md +++ b/lnbits/extensions/twitchalerts/README.md @@ -1,6 +1,6 @@

Example Extension

*tagline*

-This is an TwitchAlerts extension to help you organise and build you own. +The TwitchAlerts extension allows you to integrate Bitcoin Lightning (and on-chain) paymnents in to your existing Streamlabs alerts! Try to include an image diff --git a/lnbits/extensions/twitchalerts/__init__.py b/lnbits/extensions/twitchalerts/__init__.py index c839c2923..06fc4cb2d 100644 --- a/lnbits/extensions/twitchalerts/__init__.py +++ b/lnbits/extensions/twitchalerts/__init__.py @@ -4,7 +4,10 @@ from lnbits.db import Database db = Database("ext_twitchalerts") twitchalerts_ext: Blueprint = Blueprint( - "twitchalerts", __name__, static_folder="static", template_folder="templates" + "twitchalerts", + __name__, + static_folder="static", + template_folder="templates" ) diff --git a/lnbits/extensions/twitchalerts/crud.py b/lnbits/extensions/twitchalerts/crud.py index 39eb84aea..31d69119f 100644 --- a/lnbits/extensions/twitchalerts/crud.py +++ b/lnbits/extensions/twitchalerts/crud.py @@ -14,7 +14,19 @@ from lnbits.helpers import urlsafe_short_hash from lnbits.core.crud import get_wallet +async def get_service_redirect_uri(request, service_id): + """Return the service's redirect URI, to be given to the third party API""" + uri_base = request.scheme + "://" + uri_base += request.headers["Host"] + "/twitchalerts/api/v1" + redirect_uri = uri_base + f"/authenticate/{service_id}" + return redirect_uri + + async def get_charge_details(service_id): + """Return the default details for a satspay charge + + These might be different depending for services implemented in the future. + """ details = { "time": 1440, } @@ -39,6 +51,7 @@ async def create_donation( message: str = "", posted: bool = False, ) -> Donation: + """Create a new Donation""" await db.execute( """ INSERT INTO Donations ( @@ -70,6 +83,10 @@ async def create_donation( async def post_donation(donation_id: str) -> tuple: + """Post donations to their respective third party APIs + + If the donation has already been posted, it will not be posted again. + """ donation = await get_donation(donation_id) if not donation: return ( @@ -125,6 +142,7 @@ async def create_service( state: str = None, onchain: str = None, ) -> Service: + """Create a new Service""" result = await db.execute( """ INSERT INTO Services ( @@ -157,6 +175,12 @@ async def create_service( async def get_service(service_id: int, by_state: str = None) -> Optional[Service]: + """Return a service either by ID or, available, by state + + Each Service's donation page is reached through its "state" hash + instead of the ID, preventing accidental payments to the wrong + streamer via typos like 2 -> 3. + """ if by_state: row = await db.fetchone( "SELECT * FROM Services WHERE state = ?", @@ -171,6 +195,7 @@ async def get_service(service_id: int, async def get_services(wallet_id: str) -> Optional[list]: + """Return all services belonging assigned to the wallet_id""" rows = await db.fetchall( "SELECT * FROM Services WHERE wallet = ?", (wallet_id,) @@ -179,6 +204,7 @@ async def get_services(wallet_id: str) -> Optional[list]: async def authenticate_service(service_id, code, redirect_uri): + """Use authentication code from third party API to retreive access token""" # The API token is passed in the querystring as 'code' service = await get_service(service_id) wallet = await get_wallet(service.wallet) @@ -201,6 +227,12 @@ async def authenticate_service(service_id, code, redirect_uri): async def service_add_token(service_id, token): + """Add access token to its corresponding Service + + This also sets authenticated = 1 to make sure the token + is not overwritten. + Tokens for Streamlabs never need to be refreshed. + """ if (await get_service(service_id)).authenticated: return False await db.execute( @@ -211,6 +243,7 @@ async def service_add_token(service_id, token): async def delete_service(service_id: int) -> None: + """Delete a Service and all corresponding Donations""" await db.execute( "DELETE FROM Services WHERE id = ?", (service_id,) @@ -224,6 +257,7 @@ async def delete_service(service_id: int) -> None: async def get_donation(donation_id: str) -> Optional[Donation]: + """Return a Donation""" row = await db.fetchone( "SELECT * FROM Donations WHERE id = ?", (donation_id,) @@ -232,6 +266,7 @@ async def get_donation(donation_id: str) -> Optional[Donation]: async def get_donations(wallet_id: str) -> Optional[list]: + """Return all Donations assigned to wallet_id""" rows = await db.fetchall( "SELECT * FROM Donations WHERE wallet = ?", (wallet_id,) @@ -240,6 +275,7 @@ async def get_donations(wallet_id: str) -> Optional[list]: async def delete_donation(donation_id: str) -> None: + """Delete a Donation and its corresponding statspay charge""" await db.execute( "DELETE FROM Donations WHERE id = ?", (donation_id,) @@ -248,6 +284,7 @@ async def delete_donation(donation_id: str) -> None: async def update_donation(donation_id: str, **kwargs) -> Donation: + """Update a Donation""" q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute(f"UPDATE Donations SET {q} WHERE id = ?", (*kwargs.values(), donation_id)) @@ -258,6 +295,7 @@ async def update_donation(donation_id: str, **kwargs) -> Donation: async def update_service(service_id: str, **kwargs) -> Donation: + """Update a service""" q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute(f"UPDATE Services SET {q} WHERE id = ?", (*kwargs.values(), service_id)) diff --git a/lnbits/extensions/twitchalerts/models.py b/lnbits/extensions/twitchalerts/models.py index c1451672f..0467e6aca 100644 --- a/lnbits/extensions/twitchalerts/models.py +++ b/lnbits/extensions/twitchalerts/models.py @@ -3,15 +3,18 @@ from typing import NamedTuple, Optional class Donation(NamedTuple): - id: str + """A Donation simply contains all the necessary information about a + user's donation to a streamer + """ + id: str # This ID always corresponds to a satspay charge ID wallet: str - name: str - message: str - cur_code: str + name: str # Name of the donor + message: str # Donation message + cur_code: str # Three letter currency code accepted by Streamlabs sats: int - amount: float - service: int - posted: bool + amount: float # The donation amount after fiat conversion + service: int # The ID of the corresponding Service + posted: bool # Whether the donation has already been posted to a Service @classmethod def from_row(cls, row: Row) -> "Donation": @@ -19,16 +22,20 @@ class Donation(NamedTuple): class Service(NamedTuple): + """A Service represents an integration with a third-party API + + Currently, Streamlabs is the only supported Service. + """ id: int - state: str - twitchuser: str - client_id: str - client_secret: str + state: str # A random hash used during authentication + twitchuser: str # The Twitch streamer's username + client_id: str # Third party service Client ID + client_secret: str # Secret corresponding to the Client ID wallet: str onchain: str - servicename: str - authenticated: bool - token: Optional[int] + servicename: str # Currently, this will just always be "Streamlabs" + authenticated: bool # Whether a token (see below) has been acquired yet + token: Optional[int] # The token with which to authenticate requests @classmethod def from_row(cls, row: Row) -> "Service": diff --git a/lnbits/extensions/twitchalerts/views.py b/lnbits/extensions/twitchalerts/views.py index 7222da169..e2503790a 100644 --- a/lnbits/extensions/twitchalerts/views.py +++ b/lnbits/extensions/twitchalerts/views.py @@ -11,11 +11,13 @@ from .crud import get_service @validate_uuids(["usr"], required=True) @check_user_exists() async def index(): + """Return the extension's settings page""" return await render_template("twitchalerts/index.html", user=g.user) @twitchalerts_ext.route("/") async def donation(state): + """Return the donation form for the Service corresponding to state""" service = await get_service(0, by_state=state) if not service: abort(HTTPStatus.NOT_FOUND, "Service does not exist.") diff --git a/lnbits/extensions/twitchalerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py index 2478972c9..b548a1d4e 100644 --- a/lnbits/extensions/twitchalerts/views_api.py +++ b/lnbits/extensions/twitchalerts/views_api.py @@ -8,6 +8,7 @@ from lnbits.utils.exchange_rates import btc_price from . import twitchalerts_ext from .crud import ( get_charge_details, + get_service_redirect_uri, create_donation, post_donation, get_donation, @@ -48,11 +49,12 @@ async def api_create_service(): @twitchalerts_ext.route("/api/v1/getaccess/", methods=["GET"]) async def api_get_access(service_id): + """Redirect to Streamlabs' Approve/Decline page for API access for Service + with service_id + """ service = await get_service(service_id) if service: - uri_base = request.scheme + "://" - uri_base += request.headers["Host"] + "/twitchalerts/api/v1" - redirect_uri = uri_base + f"/authenticate/{service_id}" + redirect_uri = await get_service_redirect_uri(request, service_id) params = { "response_type": "code", "client_id": service.client_id, @@ -75,6 +77,11 @@ async def api_get_access(service_id): @twitchalerts_ext.route("/api/v1/authenticate/", methods=["GET"]) async def api_authenticate_service(service_id): + """Endpoint visited via redirect during third party API authentication + + If successful, an API access token will be added to the service, and + the user will be redirected to index.html. + """ code = request.args.get('code') state = request.args.get('state') service = await get_service(service_id) @@ -105,11 +112,13 @@ async def api_authenticate_service(service_id): } ) async def api_create_donation(): - """Takes data from donation form and creates+returns SatsPay charge""" + """Take data from donation form and return satspay charge""" + # Currency is hardcoded while frotnend is limited cur_code = "USD" - price = await btc_price(cur_code) sats = g.data["sats"] message = g.data.get("message", "") + # Fiat amount is calculated here while frontend is limited + price = await btc_price(cur_code) amount = sats * (10 ** (-8)) * price webhook_base = request.scheme + "://" + request.headers["Host"] service_id = g.data["service"] @@ -147,7 +156,7 @@ async def api_create_donation(): } ) async def api_post_donation(): - """Posts a paid donation to Stremalabs/StreamElements. + """Post a paid donation to Stremalabs/StreamElements. This endpoint acts as a webhook for the SatsPayServer extension.""" data = await request.get_json(force=True) @@ -165,6 +174,7 @@ async def api_post_donation(): @twitchalerts_ext.route("/api/v1/services", methods=["GET"]) @api_check_wallet_key("invoice") async def api_get_services(): + """Return list of all services assigned to wallet with given invoice key""" wallet_ids = (await get_user(g.wallet.user)).wallet_ids services = [] for wallet_id in wallet_ids: @@ -181,6 +191,9 @@ async def api_get_services(): @twitchalerts_ext.route("/api/v1/donations", methods=["GET"]) @api_check_wallet_key("invoice") async def api_get_donations(): + """Return list of all donations assigned to wallet with given invoice + key + """ wallet_ids = (await get_user(g.wallet.user)).wallet_ids donations = [] for wallet_id in wallet_ids: @@ -197,6 +210,7 @@ async def api_get_donations(): @twitchalerts_ext.route("/api/v1/donations/", methods=["PUT"]) @api_check_wallet_key("invoice") async def api_update_donation(donation_id=None): + """Update a donation with the data given in the request""" if donation_id: donation = await get_donation(donation_id) @@ -224,6 +238,7 @@ async def api_update_donation(donation_id=None): @twitchalerts_ext.route("/api/v1/services/", methods=["PUT"]) @api_check_wallet_key("invoice") async def api_update_service(service_id=None): + """Update a service with the data given in the request""" if service_id: service = await get_service(service_id) @@ -251,6 +266,7 @@ async def api_update_service(service_id=None): @twitchalerts_ext.route("/api/v1/donations/", methods=["DELETE"]) @api_check_wallet_key("invoice") async def api_delete_donation(donation_id): + """Delete the donation with the given donation_id""" donation = await get_donation(donation_id) if not donation: return ( @@ -270,6 +286,7 @@ async def api_delete_donation(donation_id): @twitchalerts_ext.route("/api/v1/services/", methods=["DELETE"]) @api_check_wallet_key("invoice") async def api_delete_service(service_id): + """Delete the service with the given service_id""" service = await get_service(service_id) if not service: return (