Cleanup, Comments, Docstrings

This commit is contained in:
Fitti 2021-07-03 18:01:04 +02:00
parent f88c622f94
commit 435787ad93
6 changed files with 89 additions and 22 deletions

View File

@ -1,6 +1,6 @@
<h1>Example Extension</h1>
<h2>*tagline*</h2>
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
<img src="https://i.imgur.com/9i4xcQB.png">

View File

@ -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"
)

View File

@ -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))

View File

@ -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":

View File

@ -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("/<state>")
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.")

View File

@ -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/<service_id>", 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/<service_id>", 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/<donation_id>", 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/<service_id>", 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/<donation_id>", 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/<service_id>", 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 (