Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update to lnbits 1.0.0 #8

Merged
merged 3 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"name": "Stream Alerts",
"short_description": "Bitcoin donations in stream alerts",
"tile": "/streamalerts/static/image/streamalerts.png",
"contributors": ["Fittiboy"],
"min_lnbits_version": "0.11.0"
"contributors": ["Fittiboy", "dni"],
"min_lnbits_version": "1.0.0"
}
212 changes: 69 additions & 143 deletions crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import httpx
from lnbits.core.crud import get_wallet
from lnbits.db import SQLITE, Database
from lnbits.db import Database
from lnbits.helpers import urlsafe_short_hash

from .models import CreateService, Donation, Service
from .models import CreateDonation, CreateService, Donation, Service

db = Database("ext_streamalerts")

Expand All @@ -18,59 +18,17 @@ async def get_service_redirect_uri(request, 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.
"""
service = await get_service(service_id)
assert service, f"Could not fetch service: {service_id}"

wallet_id = service.wallet
wallet = await get_wallet(wallet_id)
assert wallet, f"Could not fetch wallet: {wallet_id}"

user = wallet.user
return {
"time": 1440,
"user": user,
"lnbitswallet": wallet_id,
"onchainwallet": service.onchain,
}


async def create_donation(
donation_id: str,
wallet: str,
cur_code: str,
sats: int,
amount: float,
service: int,
name: str = "Anonymous",
message: str = "",
posted: bool = False,
data: CreateDonation, wallet: str, amount: float, donation_id: Optional[str] = None
) -> Donation:
"""Create a new Donation"""
await db.execute(
"""
INSERT INTO streamalerts.Donations (
id,
wallet,
name,
message,
cur_code,
sats,
amount,
service,
posted
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(donation_id, wallet, name, message, cur_code, sats, amount, service, posted),
donation = Donation(
id=donation_id or urlsafe_short_hash(),
amount=amount,
wallet=wallet,
**data.dict(),
)

donation = await get_donation(donation_id)
assert donation, "Newly created donation couldn't be retrieved"
await db.insert("streamalerts.donations", donation)
return donation


Expand Down Expand Up @@ -105,83 +63,54 @@ async def post_donation(donation_id: str) -> dict:
else:
return {"message": "Unsopported servicename"}
await db.execute(
"UPDATE streamalerts.Donations SET posted = ? WHERE id = ?",
(
True,
donation_id,
),
"UPDATE streamalerts.donations SET posted = :posted WHERE id = :id",
{"id": donation_id, "posted": True},
)
return response.json()


async def create_service(data: CreateService) -> Service:
"""Create a new Service"""

returning = "" if db.type == SQLITE else "RETURNING ID"
method = db.execute if db.type == SQLITE else db.fetchone

result = await method(
f"""
INSERT INTO streamalerts.Services (
twitchuser,
client_id,
client_secret,
wallet,
servicename,
authenticated,
state,
onchain
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
{returning}
""",
(
data.twitchuser,
data.client_id,
data.client_secret,
data.wallet,
data.servicename,
False,
urlsafe_short_hash(),
data.onchain,
),
service = Service(
id=urlsafe_short_hash(),
state=urlsafe_short_hash(),
**data.dict(),
)
if db.type == SQLITE:
service_id = result._result_proxy.lastrowid
else:
service_id = result[0] # type: ignore

service = await get_service(service_id)
assert service, f"Could not fetch service: {service_id}"
await db.insert("streamalerts.services", service)
return service


async def get_service(
service_id: int, by_state: Optional[str] = None
service_id: Optional[str] = None, by_state: Optional[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.
"""
assert service_id or by_state, "Must provide either service_id or by_state"
if by_state:
row = await db.fetchone(
"SELECT * FROM streamalerts.Services WHERE state = ?", (by_state,)
return await db.fetchone(
"SELECT * FROM streamalerts.services WHERE state = :state",
{"state": by_state},
Service,
)
else:
row = await db.fetchone(
"SELECT * FROM streamalerts.Services WHERE id = ?", (service_id,)
return await db.fetchone(
"SELECT * FROM streamalerts.services WHERE id = :id",
{"id": service_id},
Service,
)
return Service.from_row(row) if row else None


async def get_services(wallet_id: str) -> Optional[list]:
async def get_services(wallet_id: str) -> list[Service]:
"""Return all services belonging assigned to the wallet_id"""
rows = await db.fetchall(
"SELECT * FROM streamalerts.Services WHERE wallet = ?", (wallet_id,)
return await db.fetchall(
"SELECT * FROM streamalerts.services WHERE wallet = :wallet",
{"wallet": wallet_id},
Service,
)
return [Service.from_row(row) for row in rows] if rows else None


async def authenticate_service(service_id, code, redirect_uri):
Expand Down Expand Up @@ -220,69 +149,66 @@ async def service_add_token(service_id, token):
return False

await db.execute(
"UPDATE streamalerts.Services SET authenticated = 1, token = ? where id = ?",
(token, service_id),
"""
UPDATE streamalerts.services
SET authenticated = 1, token = :token WHERE id = :id
""",
{"id": service_id, "token": token},
)
return True


async def delete_service(service_id: int) -> list[str]:
"""Delete a Service and all corresponding Donations"""
rows = await db.fetchall(
"SELECT * FROM streamalerts.Donations WHERE service = ?", (service_id,)
async def delete_service(service_id: str) -> list[str]:
"""Delete a Service and all corresponding donations"""
donations = await db.fetchall(
"SELECT * FROM streamalerts.donations WHERE service = :service",
{"service": service_id},
Donation,
)
for row in rows:
await delete_donation(row["id"])
donation_ids = []
for donation in donations:
donation_ids.append(donation.id)
await delete_donation(donation.id)

await db.execute("DELETE FROM streamalerts.Services WHERE id = ?", (service_id,))
await db.execute(
"DELETE FROM streamalerts.services WHERE id = :id", {"id": service_id}
)

return [row["id"] for row in rows]
return donation_ids


async def get_donation(donation_id: str) -> Optional[Donation]:
"""Return a Donation"""
row = await db.fetchone(
"SELECT * FROM streamalerts.Donations WHERE id = ?", (donation_id,)
return await db.fetchone(
"SELECT * FROM streamalerts.donations WHERE id = :id",
{"id": donation_id},
Donation,
)
return Donation.from_row(row) if row else None


async def get_donations(wallet_id: str) -> Optional[list]:
"""Return all streamalerts.Donations assigned to wallet_id"""
rows = await db.fetchall(
"SELECT * FROM streamalerts.Donations WHERE wallet = ?", (wallet_id,)
async def get_donations(wallet_id: str) -> list[Donation]:
"""Return all streamalerts.donations assigned to wallet_id"""
return await db.fetchall(
"SELECT * FROM streamalerts.donations WHERE wallet = :wallet",
{"wallet": wallet_id},
Donation,
)
return [Donation.from_row(row) for row in rows] if rows else None


async def delete_donation(donation_id: str) -> None:
"""Delete a Donation and its corresponding statspay charge"""
await db.execute("DELETE FROM streamalerts.Donations WHERE id = ?", (donation_id,))
await db.execute(
"DELETE FROM streamalerts.donations WHERE id = :id", {"id": donation_id}
)


async def update_donation(donation_id: str, **kwargs) -> Donation:
async def update_donation(donation: Donation) -> Donation:
"""Update a Donation"""
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE streamalerts.Donations SET {q} WHERE id = ?",
(*kwargs.values(), donation_id),
)
row = await db.fetchone(
"SELECT * FROM streamalerts.Donations WHERE id = ?", (donation_id,)
)
assert row, "Newly updated donation couldn't be retrieved"
return Donation(**row)
await db.update("streamalerts.donations", donation)
return donation


async def update_service(service_id: str, **kwargs) -> Service:
async def update_service(service: Service) -> Service:
"""Update a service"""
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE streamalerts.Services SET {q} WHERE id = ?",
(*kwargs.values(), service_id),
)
row = await db.fetchone(
"SELECT * FROM streamalerts.Services WHERE id = ?", (service_id,)
)
assert row, "Newly updated service couldn't be retrieved"
return Service(**row)
await db.update("streamalerts.services", service)
return service
13 changes: 6 additions & 7 deletions migrations.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
async def m001_initial(db):

await db.execute(
f"""
CREATE TABLE IF NOT EXISTS streamalerts.Services (
id {db.serial_primary_key},
"""
CREATE TABLE IF NOT EXISTS streamalerts.services (
id TEXT PRIMARY KEY,
state TEXT NOT NULL,
twitchuser TEXT NOT NULL,
client_id TEXT NOT NULL,
Expand All @@ -19,17 +19,16 @@ async def m001_initial(db):

await db.execute(
f"""
CREATE TABLE IF NOT EXISTS streamalerts.Donations (
CREATE TABLE IF NOT EXISTS streamalerts.donations (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
name TEXT NOT NULL,
message TEXT NOT NULL,
cur_code TEXT NOT NULL,
sats {db.big_int} NOT NULL,
amount FLOAT NOT NULL,
service INTEGER NOT NULL,
posted BOOLEAN NOT NULL,
FOREIGN KEY(service) REFERENCES {db.references_schema}Services(id)
service TEXT NOT NULL,
posted BOOLEAN NOT NULL
);
"""
)
24 changes: 8 additions & 16 deletions models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from sqlite3 import Row
from typing import Optional

from fastapi import Query
Expand All @@ -17,8 +16,9 @@ class CreateService(BaseModel):
class CreateDonation(BaseModel):
name: str = Query("Anonymous")
sats: int = Query(..., ge=1)
service: int = Query(...)
service: str = Query(...)
message: str = Query("")
cur_code: str = Query("USD")


class ValidateDonation(BaseModel):
Expand All @@ -37,12 +37,8 @@ class Donation(BaseModel):
cur_code: str # Three letter currency code accepted by Streamlabs
sats: int
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":
return cls(**dict(row))
service: str # The ID of the corresponding Service
posted: bool = False # Whether the donation has already been posted to a Service


class Service(BaseModel):
Expand All @@ -51,20 +47,16 @@ class Service(BaseModel):
Currently, Streamlabs is the only supported Service.
"""

id: int
id: 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: Optional[str]
servicename: str # Currently, this will just always be "Streamlabs"
authenticated: bool # Whether a token (see below) has been acquired yet
token: Optional[str] # The token with which to authenticate requests

@classmethod
def from_row(cls, row: Row) -> "Service":
return cls(**dict(row))
authenticated: bool = False # Whether a token (see below) has been acquired yet
onchain: Optional[str] = None
token: Optional[str] = None # The token with which to authenticate requests


class ChargeStatus(BaseModel):
Expand Down
Loading