From fd8d4d3e670a49f38fb48e6c9905b45ef4008814 Mon Sep 17 00:00:00 2001 From: Fitti Date: Sun, 20 Jun 2021 06:34:01 +0200 Subject: [PATCH 01/40] Add TwitchAlerts plugin --- lnbits/extensions/TwitchAlerts/README.md | 11 ++++ lnbits/extensions/TwitchAlerts/__init__.py | 12 ++++ lnbits/extensions/TwitchAlerts/config.json | 6 ++ lnbits/extensions/TwitchAlerts/migrations.py | 11 ++++ lnbits/extensions/TwitchAlerts/models.py | 11 ++++ .../TwitchAlerts/templates/example/index.html | 57 +++++++++++++++++++ lnbits/extensions/TwitchAlerts/views.py | 12 ++++ lnbits/extensions/TwitchAlerts/views_api.py | 40 +++++++++++++ 8 files changed, 160 insertions(+) create mode 100644 lnbits/extensions/TwitchAlerts/README.md create mode 100644 lnbits/extensions/TwitchAlerts/__init__.py create mode 100644 lnbits/extensions/TwitchAlerts/config.json create mode 100644 lnbits/extensions/TwitchAlerts/migrations.py create mode 100644 lnbits/extensions/TwitchAlerts/models.py create mode 100644 lnbits/extensions/TwitchAlerts/templates/example/index.html create mode 100644 lnbits/extensions/TwitchAlerts/views.py create mode 100644 lnbits/extensions/TwitchAlerts/views_api.py diff --git a/lnbits/extensions/TwitchAlerts/README.md b/lnbits/extensions/TwitchAlerts/README.md new file mode 100644 index 00000000..543fc337 --- /dev/null +++ b/lnbits/extensions/TwitchAlerts/README.md @@ -0,0 +1,11 @@ +

Example Extension

+

*tagline*

+This is an TwitchAlerts extension to help you organise and build you own. + +Try to include an image + + + +

If your extension has API endpoints, include useful ones here

+ +curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"TwitchAlerts"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY" diff --git a/lnbits/extensions/TwitchAlerts/__init__.py b/lnbits/extensions/TwitchAlerts/__init__.py new file mode 100644 index 00000000..c31c5628 --- /dev/null +++ b/lnbits/extensions/TwitchAlerts/__init__.py @@ -0,0 +1,12 @@ +from quart import Blueprint +from lnbits.db import Database + +db = Database("ext_TwitchAlerts") + +TwitchAlerts_ext: Blueprint = Blueprint( + "TwitchAlerts", __name__, static_folder="static", template_folder="templates" +) + + +from .views_api import * # noqa +from .views import * # noqa diff --git a/lnbits/extensions/TwitchAlerts/config.json b/lnbits/extensions/TwitchAlerts/config.json new file mode 100644 index 00000000..55389373 --- /dev/null +++ b/lnbits/extensions/TwitchAlerts/config.json @@ -0,0 +1,6 @@ +{ + "name": "Build your own!", + "short_description": "Join us, make an extension", + "icon": "info", + "contributors": ["github_username"] +} diff --git a/lnbits/extensions/TwitchAlerts/migrations.py b/lnbits/extensions/TwitchAlerts/migrations.py new file mode 100644 index 00000000..2d107d19 --- /dev/null +++ b/lnbits/extensions/TwitchAlerts/migrations.py @@ -0,0 +1,11 @@ +# async def m001_initial(db): + +# await db.execute( +# """ +# CREATE TABLE IF NOT EXISTS TwitchAlerts ( +# id TEXT PRIMARY KEY, +# wallet TEXT NOT NULL, +# time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) +# ); +# """ +# ) diff --git a/lnbits/extensions/TwitchAlerts/models.py b/lnbits/extensions/TwitchAlerts/models.py new file mode 100644 index 00000000..be523233 --- /dev/null +++ b/lnbits/extensions/TwitchAlerts/models.py @@ -0,0 +1,11 @@ +# from sqlite3 import Row +# from typing import NamedTuple + + +# class Example(NamedTuple): +# id: str +# wallet: str +# +# @classmethod +# def from_row(cls, row: Row) -> "Example": +# return cls(**dict(row)) diff --git a/lnbits/extensions/TwitchAlerts/templates/example/index.html b/lnbits/extensions/TwitchAlerts/templates/example/index.html new file mode 100644 index 00000000..e78e07bc --- /dev/null +++ b/lnbits/extensions/TwitchAlerts/templates/example/index.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} + + +
Frameworks used by LNbits
+ + + {% raw %} + + + {{ tool.name }} + {{ tool.language }} + + {% endraw %} + + + +

+ A magical "g" is always available, with info about the user, wallets and + extensions: +

+ {% raw %}{{ g }}{% endraw %} +
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/TwitchAlerts/views.py b/lnbits/extensions/TwitchAlerts/views.py new file mode 100644 index 00000000..f080a62e --- /dev/null +++ b/lnbits/extensions/TwitchAlerts/views.py @@ -0,0 +1,12 @@ +from quart import g, render_template + +from lnbits.decorators import check_user_exists, validate_uuids + +from . import TwitchAlerts_ext + + +@TwitchAlerts_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("TwitchAlerts/index.html", user=g.user) diff --git a/lnbits/extensions/TwitchAlerts/views_api.py b/lnbits/extensions/TwitchAlerts/views_api.py new file mode 100644 index 00000000..1641d5eb --- /dev/null +++ b/lnbits/extensions/TwitchAlerts/views_api.py @@ -0,0 +1,40 @@ +# views_api.py is for you API endpoints that could be hit by another service + +# add your dependencies here + +# import json +# import httpx +# (use httpx just like requests, except instead of response.ok there's only the +# response.is_error that is its inverse) + +from quart import jsonify +from http import HTTPStatus + +from . import TwitchAlerts_ext + + +# add your endpoints here + + +@TwitchAlerts_ext.route("/api/v1/tools", methods=["GET"]) +async def api_TwitchAlerts(): + """Try to add descriptions for others.""" + tools = [ + { + "name": "Quart", + "url": "https://pgjones.gitlab.io/quart/", + "language": "Python", + }, + { + "name": "Vue.js", + "url": "https://vuejs.org/", + "language": "JavaScript", + }, + { + "name": "Quasar Framework", + "url": "https://quasar.dev/", + "language": "JavaScript", + }, + ] + + return jsonify(tools), HTTPStatus.OK From 420a4cd524bc58faf16eb50ad145b5b553bc8198 Mon Sep 17 00:00:00 2001 From: Fitti Date: Sun, 20 Jun 2021 07:50:58 +0200 Subject: [PATCH 02/40] Make extenstion available to load --- lnbits/extensions/TwitchAlerts/config.json | 8 +-- .../TwitchAlerts/templates/example/index.html | 57 ------------------- 2 files changed, 4 insertions(+), 61 deletions(-) delete mode 100644 lnbits/extensions/TwitchAlerts/templates/example/index.html diff --git a/lnbits/extensions/TwitchAlerts/config.json b/lnbits/extensions/TwitchAlerts/config.json index 55389373..f9105475 100644 --- a/lnbits/extensions/TwitchAlerts/config.json +++ b/lnbits/extensions/TwitchAlerts/config.json @@ -1,6 +1,6 @@ { - "name": "Build your own!", - "short_description": "Join us, make an extension", - "icon": "info", - "contributors": ["github_username"] + "name": "Twitch Alerts", + "short_description": "Integrate Bitcoin donations into your stream alerts!", + "icon": "notifications_active", + "contributors": ["Fittiboy"] } diff --git a/lnbits/extensions/TwitchAlerts/templates/example/index.html b/lnbits/extensions/TwitchAlerts/templates/example/index.html deleted file mode 100644 index e78e07bc..00000000 --- a/lnbits/extensions/TwitchAlerts/templates/example/index.html +++ /dev/null @@ -1,57 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} - - -
Frameworks used by LNbits
- - - {% raw %} - - - {{ tool.name }} - {{ tool.language }} - - {% endraw %} - - - -

- A magical "g" is always available, with info about the user, wallets and - extensions: -

- {% raw %}{{ g }}{% endraw %} -
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} From da893ccba6840fbca15a1dc7cbed33863985d86c Mon Sep 17 00:00:00 2001 From: Fitti Date: Sun, 20 Jun 2021 07:51:24 +0200 Subject: [PATCH 03/40] Fix missing template --- .../templates/TwitchAlerts/index.html | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 lnbits/extensions/TwitchAlerts/templates/TwitchAlerts/index.html diff --git a/lnbits/extensions/TwitchAlerts/templates/TwitchAlerts/index.html b/lnbits/extensions/TwitchAlerts/templates/TwitchAlerts/index.html new file mode 100644 index 00000000..e78e07bc --- /dev/null +++ b/lnbits/extensions/TwitchAlerts/templates/TwitchAlerts/index.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} + + +
Frameworks used by LNbits
+ + + {% raw %} + + + {{ tool.name }} + {{ tool.language }} + + {% endraw %} + + + +

+ A magical "g" is always available, with info about the user, wallets and + extensions: +

+ {% raw %}{{ g }}{% endraw %} +
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} From 2dded1b58500c99f231aad94c627b9cb18c52f99 Mon Sep 17 00:00:00 2001 From: Fitti Date: Tue, 22 Jun 2021 09:55:58 +0200 Subject: [PATCH 04/40] Get basic working endpoints This is a setup that works specifically with a hardcoded test wallet --- lnbits/extensions/TwitchAlerts/crud.py | 9 +++ lnbits/extensions/TwitchAlerts/migrations.py | 20 ++--- lnbits/extensions/TwitchAlerts/views_api.py | 79 +++++++++++++------- 3 files changed, 71 insertions(+), 37 deletions(-) create mode 100644 lnbits/extensions/TwitchAlerts/crud.py diff --git a/lnbits/extensions/TwitchAlerts/crud.py b/lnbits/extensions/TwitchAlerts/crud.py new file mode 100644 index 00000000..6027e557 --- /dev/null +++ b/lnbits/extensions/TwitchAlerts/crud.py @@ -0,0 +1,9 @@ +async def get_charge_details(service): + details = { + "user": "8ee4d2d2b788467ebbab172ed3d50aaa", + "description": "Testing", + "lnbitswallet": "e1e7c4f4f86a4905ab413f96fb78aabc", + "time": 1440, + "amount": 1 + } + return details diff --git a/lnbits/extensions/TwitchAlerts/migrations.py b/lnbits/extensions/TwitchAlerts/migrations.py index 2d107d19..db0a9c7c 100644 --- a/lnbits/extensions/TwitchAlerts/migrations.py +++ b/lnbits/extensions/TwitchAlerts/migrations.py @@ -1,11 +1,11 @@ -# async def m001_initial(db): +async def m001_initial(db): -# await db.execute( -# """ -# CREATE TABLE IF NOT EXISTS TwitchAlerts ( -# id TEXT PRIMARY KEY, -# wallet TEXT NOT NULL, -# time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) -# ); -# """ -# ) + await db.execute( + """ + CREATE TABLE IF NOT EXISTS TwitchAlerts ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) + ); + """ + ) diff --git a/lnbits/extensions/TwitchAlerts/views_api.py b/lnbits/extensions/TwitchAlerts/views_api.py index 1641d5eb..f4656d45 100644 --- a/lnbits/extensions/TwitchAlerts/views_api.py +++ b/lnbits/extensions/TwitchAlerts/views_api.py @@ -1,40 +1,65 @@ -# views_api.py is for you API endpoints that could be hit by another service - -# add your dependencies here - # import json # import httpx # (use httpx just like requests, except instead of response.ok there's only the # response.is_error that is its inverse) -from quart import jsonify +from quart import g, redirect, request # jsonify from http import HTTPStatus +from lnbits.decorators import api_validate_post_request, api_check_wallet_key + from . import TwitchAlerts_ext +from .crud import get_charge_details +from ..satspay.crud import create_charge, get_charge, delete_charge -# add your endpoints here +@TwitchAlerts_ext.route("/api/v1/createdonation", methods=["POST"]) +@api_check_wallet_key("invoice") +@api_validate_post_request( + schema={ + "name": {"type": "string"}, + "sats": {"type": "integer", "required": True}, + "service": {"type": "string", "required": True}, + "cur_code": {"type": "string", "required": True}, + "amount": {"type": "float", "required": True} + } +) +async def api_create_donation(): + """Takes data from donation form and creates+returns SatsPay charge""" + webhook_base = request.scheme + "://" + request.headers["Host"] + charge_details = await get_charge_details(g.data["service"]) + charge = await create_charge( + webhook=webhook_base + "/TwitchAlerts/api/v1/postdonation", + **charge_details) + return redirect(f"/satspay/{charge.id}") -@TwitchAlerts_ext.route("/api/v1/tools", methods=["GET"]) -async def api_TwitchAlerts(): - """Try to add descriptions for others.""" - tools = [ - { - "name": "Quart", - "url": "https://pgjones.gitlab.io/quart/", - "language": "Python", - }, - { - "name": "Vue.js", - "url": "https://vuejs.org/", - "language": "JavaScript", - }, - { - "name": "Quasar Framework", - "url": "https://quasar.dev/", - "language": "JavaScript", - }, - ] +@TwitchAlerts_ext.route("/api/v1/postdonation", methods=["POST"]) +# @api_validate_post_request( +# schema={ +# "id": {"type": "string", "required": True}, +# "description": {"type": "string", "allow_unknown": True}, +# "onchainaddress": {"type": "string", "allow_unknown": True}, +# "payment_request": {"type": "string", "allow_unknown": True}, +# "payment_hash": {"type": "string", "allow_unknown": True}, +# "time": {"type": "integer", "allow_unknown": True}, +# "amount": {"type": "integer", "allow_unknown": True}, +# "paid": {"type": "boolean", "allow_unknown": True}, +# "timestamp": {"type": "integer", "allow_unknown": True}, +# "completelink": {"type": "string", "allow_unknown": True}, +# } +# ) +async def api_post_donation(): + """Posts a paid donation to Stremalabs/StreamElements. - return jsonify(tools), HTTPStatus.OK + This endpoint acts as a webhook for the SatsPayServer extension.""" + data = await request.get_json(force=True) + charge_id = data["id"] + charge = await get_charge(charge_id) + print(charge) + if charge and charge.paid: + await delete_charge(charge_id) + print("This endpoint works!") + return "", HTTPStatus.OK + else: + return "", HTTPStatus.OK From c759f3617e9e9441cb4fa76d56a924432d352de0 Mon Sep 17 00:00:00 2001 From: Fitti Date: Tue, 22 Jun 2021 13:18:50 +0200 Subject: [PATCH 05/40] Add models and migrations --- lnbits/extensions/TwitchAlerts/migrations.py | 29 +++++++++++++-- lnbits/extensions/TwitchAlerts/models.py | 39 +++++++++++++++----- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/lnbits/extensions/TwitchAlerts/migrations.py b/lnbits/extensions/TwitchAlerts/migrations.py index db0a9c7c..39d7f0ae 100644 --- a/lnbits/extensions/TwitchAlerts/migrations.py +++ b/lnbits/extensions/TwitchAlerts/migrations.py @@ -2,10 +2,31 @@ async def m001_initial(db): await db.execute( """ - CREATE TABLE IF NOT EXISTS TwitchAlerts ( - id TEXT PRIMARY KEY, + CREATE TABLE IF NOT EXISTS Services ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + twitchuser TEXT NOT NULL, + client_id TEXT NOT NULL, + client_secret TEXT NOT NULL, wallet TEXT NOT NULL, - time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) + onchain TEXT, + servicename TEXT NOT NULL, + authenticated BOOLEAN NOT NULL, + token TEXT ); - """ + """ + ) + + await db.execute( + """ + CREATE TABLE IF NOT EXISTS Donations ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + cur_code TEXT NOT NULL, + sats INT NOT NULL, + amount FLOAT NOT NULL, + service INTEGER NOT NULL, + posted BOOLEAN NOT NULL, + FOREIGN KEY(service) REFERENCES Services(id) + ); + """ ) diff --git a/lnbits/extensions/TwitchAlerts/models.py b/lnbits/extensions/TwitchAlerts/models.py index be523233..ce601d5a 100644 --- a/lnbits/extensions/TwitchAlerts/models.py +++ b/lnbits/extensions/TwitchAlerts/models.py @@ -1,11 +1,32 @@ -# from sqlite3 import Row -# from typing import NamedTuple +from sqlite3 import Row +from typing import NamedTuple, Optional -# class Example(NamedTuple): -# id: str -# wallet: str -# -# @classmethod -# def from_row(cls, row: Row) -> "Example": -# return cls(**dict(row)) +class Donations(NamedTuple): + id: str + name: str + cur_code: str + sats: int + amount: float + service: int + posted: bool + + @classmethod + def from_row(cls, row: Row) -> "Donations": + return cls(**dict(row)) + + +class Services(NamedTuple): + id: int + twitchuser: str + client_id: str + client_secret: str + wallet: str + onchain: str + servicename: str + authenticated: bool + token: Optional[int] + + @classmethod + def from_row(cls, row: Row) -> "Services": + return cls(**dict(row)) From 3db23d954ba79a1d1af181f48cb2accc0f3df0a4 Mon Sep 17 00:00:00 2001 From: Fitti Date: Tue, 22 Jun 2021 13:19:22 +0200 Subject: [PATCH 06/40] Create various helper functions --- lnbits/extensions/TwitchAlerts/crud.py | 179 ++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 6 deletions(-) diff --git a/lnbits/extensions/TwitchAlerts/crud.py b/lnbits/extensions/TwitchAlerts/crud.py index 6027e557..289dee85 100644 --- a/lnbits/extensions/TwitchAlerts/crud.py +++ b/lnbits/extensions/TwitchAlerts/crud.py @@ -1,9 +1,176 @@ -async def get_charge_details(service): +from . import db +from .models import Donations, Services + +from ..satspay.crud import delete_charge + +import httpx + +from typing import Optional +from lnbits.core.crud import get_wallet + + +async def get_charge_details(service_id): details = { - "user": "8ee4d2d2b788467ebbab172ed3d50aaa", - "description": "Testing", - "lnbitswallet": "e1e7c4f4f86a4905ab413f96fb78aabc", + "description": f"TwitchAlerts donation for service {str(service_id)}", "time": 1440, - "amount": 1 - } + } + service = get_service(service_id) + wallet_id = service.wallet + wallet = await get_wallet(wallet_id) + user = wallet.user + details["user"] = user + details["lnbitswallet"] = wallet_id + details["onchainwallet"] = service.onchain return details + + +async def create_donation( + id: str, + name: str, + cur_code: str, + sats: int, + amount: float, + service: int, + posted: bool = False, +) -> Donations: + await db.execute( + """ + INSERT INTO Donations ( + id, + name, + cur_code, + sats, + amount, + service, + posted + ) + VALUES (?, ?, ?) + """, + ( + id, + name, + cur_code, + sats, + amount, + service, + posted + ), + ) + return await get_donation(id) + + +async def post_donation(donation_id: str) -> None: + donation = await get_donation(donation_id) + if donation.posted: + return False + service = await get_service(donation.service) + servicename = service.servicename + if servicename == "Streamlabs": + pass + await db.execute( + "UPDATE Donations SET posted = 1 WHERE id = ?", + (donation_id,) + ) + return True + + +async def create_service( + twitchuser: str, + client_id: str, + client_secret: str, + wallet: str, + servicename: str, + onchain: str = None, +) -> Services: + result = await db.execute( + """ + INSERT INTO Services ( + twitchuser, + client_id, + client_secret, + wallet, + servicename, + authenticated, + onchain + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + twitchuser, + client_id, + client_secret, + wallet, + servicename, + False, + onchain, + ), + ) + service_id = result._result_proxy.lastrowid + service = await get_service(service_id) + return service + + +async def get_service(service_id: int) -> Optional[Services]: + row = await db.fetchone( + "SELECT * FROM Services WHERE id = ?", + (service_id,) + ) + return Services.from_row(row) if row else None + + +async def authenticate_service(service_id, code, redirect_uri): + # The API token is passed in the querystring as 'code' + service = await get_service(service_id) + wallet = await get_wallet(service.wallet) + user = wallet.user + url = "https://streamlabs.com/api/v1.0/token" + data = { + "grant_type": "authorization_code", + "code": code, + "client_id": service.client_id, + "client_secret": service.client_secret, + "redirect_uri": redirect_uri, + } + print(data) + async with httpx.AsyncClient() as client: + response = (await client.post(url, data=data)).json() + print(response) + token = response['access_token'] + await service_add_token(service_id, token) + return f"/TwitchAlerts/?usr={user}" + + +async def service_add_token(service_id, token): + db.execute( + "UPDATE Services SET token = ? where id = ?", + (token, service_id,), + ) + + +async def delete_service(service_id: int) -> None: + await db.execute( + "DELETE FROM Services WHERE id = ?", + (service_id,) + ) + rows = await db.fetchall( + "SELECT * FROM Donations WHERE service = ?", + (service_id,) + ) + for row in rows: + await delete_donation(row["id"]) + + +async def get_donation(donation_id: str) -> Optional[Donations]: + row = await db.fetchone( + "SELECT * FROM Donations WHERE id = ?", + (donation_id,) + ) + return Donations.from_row(row) if row else None + + +async def delete_donation(donation_id: str) -> None: + await db.execute( + "DELETE FROM Donatoins WHERE id = ?", + (donation_id,) + ) + await delete_charge(donation_id) From 74af2c0b026296258a75149396b8fb542be7f993 Mon Sep 17 00:00:00 2001 From: Fitti Date: Tue, 22 Jun 2021 13:19:45 +0200 Subject: [PATCH 07/40] Expand API endpoints --- lnbits/extensions/TwitchAlerts/views_api.py | 72 +++++++++++++++++---- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/lnbits/extensions/TwitchAlerts/views_api.py b/lnbits/extensions/TwitchAlerts/views_api.py index f4656d45..75b654fc 100644 --- a/lnbits/extensions/TwitchAlerts/views_api.py +++ b/lnbits/extensions/TwitchAlerts/views_api.py @@ -1,16 +1,46 @@ -# import json -# import httpx -# (use httpx just like requests, except instead of response.ok there's only the -# response.is_error that is its inverse) - -from quart import g, redirect, request # jsonify +from quart import g, redirect, request from http import HTTPStatus from lnbits.decorators import api_validate_post_request, api_check_wallet_key from . import TwitchAlerts_ext -from .crud import get_charge_details -from ..satspay.crud import create_charge, get_charge, delete_charge +from .crud import ( + get_charge_details, + create_donation, + post_donation, + create_service, + authenticate_service +) +from ..satspay.crud import create_charge, get_charge + + +@TwitchAlerts_ext.route("/api/v1/createservice", methods=["POST"]) +@api_check_wallet_key("invoice") +@api_validate_post_request( + schema={ + "twitchuser": {"type": "string", "required": True}, + "client_id": {"type": "string", "required": True}, + "client_secret": {"type": "string", "required": True}, + "wallet": {"type": "string", "required": True}, + "servicename": {"type": "string", "required": True}, + "onchain": {"type": "string"} + } +) +async def api_create_service(): + """Create a service, which holds data about how/where to post donations""" + service = await create_service(**g.data) + redirect_url = request.scheme + "://" + request.headers["Host"] + redirect_url += f"/TwitchAlerts/?created={str(service.id)}" + return redirect(redirect_url) + + +@TwitchAlerts_ext.route("/api/v1/authenticate/", methods=["GET"]) +async def api_authenticate_service(service_id): + code = request.args.get('code') + redirect_uri = request.scheme + "://" + request.headers["Host"] + redirect_uri += f"/TwitchAlerts/api/v1/authenticate/{service_id}" + url = await authenticate_service(service_id, code, redirect_uri) + return redirect(url) @TwitchAlerts_ext.route("/api/v1/createdonation", methods=["POST"]) @@ -19,7 +49,7 @@ from ..satspay.crud import create_charge, get_charge, delete_charge schema={ "name": {"type": "string"}, "sats": {"type": "integer", "required": True}, - "service": {"type": "string", "required": True}, + "service": {"type": "integer", "required": True}, "cur_code": {"type": "string", "required": True}, "amount": {"type": "float", "required": True} } @@ -28,9 +58,21 @@ async def api_create_donation(): """Takes data from donation form and creates+returns SatsPay charge""" webhook_base = request.scheme + "://" + request.headers["Host"] charge_details = await get_charge_details(g.data["service"]) + name = g.data.get("name", "Anonymous") charge = await create_charge( + amount=g.data["sats"], + completelink="https://twitch.tv/Fitti", + completelinktext="Back to Stream!", webhook=webhook_base + "/TwitchAlerts/api/v1/postdonation", **charge_details) + await create_donation( + id=charge.id, + name=name, + cur_code=g.data["cur_code"], + sats=g.data["sats"], + amount=g.data["amount"], + service=g.data["service"], + ) return redirect(f"/satspay/{charge.id}") @@ -54,12 +96,14 @@ async def api_post_donation(): This endpoint acts as a webhook for the SatsPayServer extension.""" data = await request.get_json(force=True) - charge_id = data["id"] - charge = await get_charge(charge_id) + donation_id = data.get("id", "No ID") + charge = await get_charge(donation_id) print(charge) if charge and charge.paid: - await delete_charge(charge_id) print("This endpoint works!") - return "", HTTPStatus.OK + if await post_donation(donation_id): + return "Posted!", HTTPStatus.OK + else: + return "Already posted!", HTTPStatus.OK else: - return "", HTTPStatus.OK + return "Not a paid charge!", HTTPStatus.OK From 9c2c04dad26cb96d24dfbf74ccb3fca74046ab6a Mon Sep 17 00:00:00 2001 From: Fitti Date: Tue, 22 Jun 2021 13:24:11 +0200 Subject: [PATCH 08/40] Fix return type for post_donation --- lnbits/extensions/TwitchAlerts/crud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/TwitchAlerts/crud.py b/lnbits/extensions/TwitchAlerts/crud.py index 289dee85..a7024ad8 100644 --- a/lnbits/extensions/TwitchAlerts/crud.py +++ b/lnbits/extensions/TwitchAlerts/crud.py @@ -59,7 +59,7 @@ async def create_donation( return await get_donation(id) -async def post_donation(donation_id: str) -> None: +async def post_donation(donation_id: str) -> bool: donation = await get_donation(donation_id) if donation.posted: return False From 76438f556350e54da56a0479f64dd342ec462707 Mon Sep 17 00:00:00 2001 From: Fitti Date: Tue, 22 Jun 2021 15:46:46 +0200 Subject: [PATCH 09/40] Make CamelCase lowercase --- lnbits/extensions/TwitchAlerts/__init__.py | 6 +++--- lnbits/extensions/TwitchAlerts/crud.py | 2 +- .../{TwitchAlerts => twitchalerts}/index.html | 2 +- lnbits/extensions/TwitchAlerts/views.py | 6 +++--- lnbits/extensions/TwitchAlerts/views_api.py | 16 ++++++++-------- 5 files changed, 16 insertions(+), 16 deletions(-) rename lnbits/extensions/TwitchAlerts/templates/{TwitchAlerts => twitchalerts}/index.html (97%) diff --git a/lnbits/extensions/TwitchAlerts/__init__.py b/lnbits/extensions/TwitchAlerts/__init__.py index c31c5628..c839c292 100644 --- a/lnbits/extensions/TwitchAlerts/__init__.py +++ b/lnbits/extensions/TwitchAlerts/__init__.py @@ -1,10 +1,10 @@ from quart import Blueprint from lnbits.db import Database -db = Database("ext_TwitchAlerts") +db = Database("ext_twitchalerts") -TwitchAlerts_ext: Blueprint = Blueprint( - "TwitchAlerts", __name__, static_folder="static", template_folder="templates" +twitchalerts_ext: Blueprint = Blueprint( + "twitchalerts", __name__, static_folder="static", template_folder="templates" ) diff --git a/lnbits/extensions/TwitchAlerts/crud.py b/lnbits/extensions/TwitchAlerts/crud.py index a7024ad8..fb3a64de 100644 --- a/lnbits/extensions/TwitchAlerts/crud.py +++ b/lnbits/extensions/TwitchAlerts/crud.py @@ -137,7 +137,7 @@ async def authenticate_service(service_id, code, redirect_uri): print(response) token = response['access_token'] await service_add_token(service_id, token) - return f"/TwitchAlerts/?usr={user}" + return f"/twitchalerts/?usr={user}" async def service_add_token(service_id, token): diff --git a/lnbits/extensions/TwitchAlerts/templates/TwitchAlerts/index.html b/lnbits/extensions/TwitchAlerts/templates/twitchalerts/index.html similarity index 97% rename from lnbits/extensions/TwitchAlerts/templates/TwitchAlerts/index.html rename to lnbits/extensions/TwitchAlerts/templates/twitchalerts/index.html index e78e07bc..ac30a196 100644 --- a/lnbits/extensions/TwitchAlerts/templates/TwitchAlerts/index.html +++ b/lnbits/extensions/TwitchAlerts/templates/twitchalerts/index.html @@ -44,7 +44,7 @@ // axios is available for making requests axios({ method: 'GET', - url: '/TwitchAlerts/api/v1/tools', + url: '/twitchalerts/api/v1/tools', headers: { 'X-TwitchAlerts-header': 'not-used' } diff --git a/lnbits/extensions/TwitchAlerts/views.py b/lnbits/extensions/TwitchAlerts/views.py index f080a62e..dd99ae85 100644 --- a/lnbits/extensions/TwitchAlerts/views.py +++ b/lnbits/extensions/TwitchAlerts/views.py @@ -2,11 +2,11 @@ from quart import g, render_template from lnbits.decorators import check_user_exists, validate_uuids -from . import TwitchAlerts_ext +from . import twitchalerts_ext -@TwitchAlerts_ext.route("/") +@twitchalerts_ext.route("/") @validate_uuids(["usr"], required=True) @check_user_exists() async def index(): - return await render_template("TwitchAlerts/index.html", user=g.user) + return await render_template("twitchalerts/index.html", user=g.user) diff --git a/lnbits/extensions/TwitchAlerts/views_api.py b/lnbits/extensions/TwitchAlerts/views_api.py index 75b654fc..893b53e8 100644 --- a/lnbits/extensions/TwitchAlerts/views_api.py +++ b/lnbits/extensions/TwitchAlerts/views_api.py @@ -3,7 +3,7 @@ from http import HTTPStatus from lnbits.decorators import api_validate_post_request, api_check_wallet_key -from . import TwitchAlerts_ext +from . import twitchalerts_ext from .crud import ( get_charge_details, create_donation, @@ -14,7 +14,7 @@ from .crud import ( from ..satspay.crud import create_charge, get_charge -@TwitchAlerts_ext.route("/api/v1/createservice", methods=["POST"]) +@twitchalerts_ext.route("/api/v1/createservice", methods=["POST"]) @api_check_wallet_key("invoice") @api_validate_post_request( schema={ @@ -30,20 +30,20 @@ async def api_create_service(): """Create a service, which holds data about how/where to post donations""" service = await create_service(**g.data) redirect_url = request.scheme + "://" + request.headers["Host"] - redirect_url += f"/TwitchAlerts/?created={str(service.id)}" + redirect_url += f"/twitchalerts/?created={str(service.id)}" return redirect(redirect_url) -@TwitchAlerts_ext.route("/api/v1/authenticate/", methods=["GET"]) +@twitchalerts_ext.route("/api/v1/authenticate/", methods=["GET"]) async def api_authenticate_service(service_id): code = request.args.get('code') redirect_uri = request.scheme + "://" + request.headers["Host"] - redirect_uri += f"/TwitchAlerts/api/v1/authenticate/{service_id}" + redirect_uri += f"/twitchalerts/api/v1/authenticate/{service_id}" url = await authenticate_service(service_id, code, redirect_uri) return redirect(url) -@TwitchAlerts_ext.route("/api/v1/createdonation", methods=["POST"]) +@twitchalerts_ext.route("/api/v1/createdonation", methods=["POST"]) @api_check_wallet_key("invoice") @api_validate_post_request( schema={ @@ -63,7 +63,7 @@ async def api_create_donation(): amount=g.data["sats"], completelink="https://twitch.tv/Fitti", completelinktext="Back to Stream!", - webhook=webhook_base + "/TwitchAlerts/api/v1/postdonation", + webhook=webhook_base + "/twitchalerts/api/v1/postdonation", **charge_details) await create_donation( id=charge.id, @@ -76,7 +76,7 @@ async def api_create_donation(): return redirect(f"/satspay/{charge.id}") -@TwitchAlerts_ext.route("/api/v1/postdonation", methods=["POST"]) +@twitchalerts_ext.route("/api/v1/postdonation", methods=["POST"]) # @api_validate_post_request( # schema={ # "id": {"type": "string", "required": True}, From 7066f4d11a017cdeaaf6861a131b77e87be8d170 Mon Sep 17 00:00:00 2001 From: Fitti Date: Tue, 22 Jun 2021 15:54:34 +0200 Subject: [PATCH 10/40] Make extension directory lowercase --- lnbits/extensions/{TwitchAlerts => twitchalerts}/README.md | 0 lnbits/extensions/{TwitchAlerts => twitchalerts}/__init__.py | 0 lnbits/extensions/{TwitchAlerts => twitchalerts}/config.json | 0 lnbits/extensions/{TwitchAlerts => twitchalerts}/crud.py | 0 lnbits/extensions/{TwitchAlerts => twitchalerts}/migrations.py | 0 lnbits/extensions/{TwitchAlerts => twitchalerts}/models.py | 0 .../templates/twitchalerts/index.html | 0 lnbits/extensions/{TwitchAlerts => twitchalerts}/views.py | 0 lnbits/extensions/{TwitchAlerts => twitchalerts}/views_api.py | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename lnbits/extensions/{TwitchAlerts => twitchalerts}/README.md (100%) rename lnbits/extensions/{TwitchAlerts => twitchalerts}/__init__.py (100%) rename lnbits/extensions/{TwitchAlerts => twitchalerts}/config.json (100%) rename lnbits/extensions/{TwitchAlerts => twitchalerts}/crud.py (100%) rename lnbits/extensions/{TwitchAlerts => twitchalerts}/migrations.py (100%) rename lnbits/extensions/{TwitchAlerts => twitchalerts}/models.py (100%) rename lnbits/extensions/{TwitchAlerts => twitchalerts}/templates/twitchalerts/index.html (100%) rename lnbits/extensions/{TwitchAlerts => twitchalerts}/views.py (100%) rename lnbits/extensions/{TwitchAlerts => twitchalerts}/views_api.py (100%) diff --git a/lnbits/extensions/TwitchAlerts/README.md b/lnbits/extensions/twitchalerts/README.md similarity index 100% rename from lnbits/extensions/TwitchAlerts/README.md rename to lnbits/extensions/twitchalerts/README.md diff --git a/lnbits/extensions/TwitchAlerts/__init__.py b/lnbits/extensions/twitchalerts/__init__.py similarity index 100% rename from lnbits/extensions/TwitchAlerts/__init__.py rename to lnbits/extensions/twitchalerts/__init__.py diff --git a/lnbits/extensions/TwitchAlerts/config.json b/lnbits/extensions/twitchalerts/config.json similarity index 100% rename from lnbits/extensions/TwitchAlerts/config.json rename to lnbits/extensions/twitchalerts/config.json diff --git a/lnbits/extensions/TwitchAlerts/crud.py b/lnbits/extensions/twitchalerts/crud.py similarity index 100% rename from lnbits/extensions/TwitchAlerts/crud.py rename to lnbits/extensions/twitchalerts/crud.py diff --git a/lnbits/extensions/TwitchAlerts/migrations.py b/lnbits/extensions/twitchalerts/migrations.py similarity index 100% rename from lnbits/extensions/TwitchAlerts/migrations.py rename to lnbits/extensions/twitchalerts/migrations.py diff --git a/lnbits/extensions/TwitchAlerts/models.py b/lnbits/extensions/twitchalerts/models.py similarity index 100% rename from lnbits/extensions/TwitchAlerts/models.py rename to lnbits/extensions/twitchalerts/models.py diff --git a/lnbits/extensions/TwitchAlerts/templates/twitchalerts/index.html b/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html similarity index 100% rename from lnbits/extensions/TwitchAlerts/templates/twitchalerts/index.html rename to lnbits/extensions/twitchalerts/templates/twitchalerts/index.html diff --git a/lnbits/extensions/TwitchAlerts/views.py b/lnbits/extensions/twitchalerts/views.py similarity index 100% rename from lnbits/extensions/TwitchAlerts/views.py rename to lnbits/extensions/twitchalerts/views.py diff --git a/lnbits/extensions/TwitchAlerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py similarity index 100% rename from lnbits/extensions/TwitchAlerts/views_api.py rename to lnbits/extensions/twitchalerts/views_api.py From 010d59caacfd771cd3b2492c3dee6a8b4ff354fa Mon Sep 17 00:00:00 2001 From: Fitti Date: Tue, 22 Jun 2021 16:42:50 +0200 Subject: [PATCH 11/40] Make class names singular --- lnbits/extensions/twitchalerts/crud.py | 16 ++++++++-------- lnbits/extensions/twitchalerts/models.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lnbits/extensions/twitchalerts/crud.py b/lnbits/extensions/twitchalerts/crud.py index fb3a64de..298c5680 100644 --- a/lnbits/extensions/twitchalerts/crud.py +++ b/lnbits/extensions/twitchalerts/crud.py @@ -1,5 +1,5 @@ from . import db -from .models import Donations, Services +from .models import Donation, Service from ..satspay.crud import delete_charge @@ -32,7 +32,7 @@ async def create_donation( amount: float, service: int, posted: bool = False, -) -> Donations: +) -> Donation: await db.execute( """ INSERT INTO Donations ( @@ -81,7 +81,7 @@ async def create_service( wallet: str, servicename: str, onchain: str = None, -) -> Services: +) -> Service: result = await db.execute( """ INSERT INTO Services ( @@ -110,12 +110,12 @@ async def create_service( return service -async def get_service(service_id: int) -> Optional[Services]: +async def get_service(service_id: int) -> Optional[Service]: row = await db.fetchone( "SELECT * FROM Services WHERE id = ?", (service_id,) ) - return Services.from_row(row) if row else None + return Service.from_row(row) if row else None async def authenticate_service(service_id, code, redirect_uri): @@ -160,17 +160,17 @@ async def delete_service(service_id: int) -> None: await delete_donation(row["id"]) -async def get_donation(donation_id: str) -> Optional[Donations]: +async def get_donation(donation_id: str) -> Optional[Donation]: row = await db.fetchone( "SELECT * FROM Donations WHERE id = ?", (donation_id,) ) - return Donations.from_row(row) if row else None + return Donation.from_row(row) if row else None async def delete_donation(donation_id: str) -> None: await db.execute( - "DELETE FROM Donatoins WHERE id = ?", + "DELETE FROM Donations WHERE id = ?", (donation_id,) ) await delete_charge(donation_id) diff --git a/lnbits/extensions/twitchalerts/models.py b/lnbits/extensions/twitchalerts/models.py index ce601d5a..827b8d8d 100644 --- a/lnbits/extensions/twitchalerts/models.py +++ b/lnbits/extensions/twitchalerts/models.py @@ -2,7 +2,7 @@ from sqlite3 import Row from typing import NamedTuple, Optional -class Donations(NamedTuple): +class Donation(NamedTuple): id: str name: str cur_code: str @@ -12,11 +12,11 @@ class Donations(NamedTuple): posted: bool @classmethod - def from_row(cls, row: Row) -> "Donations": + def from_row(cls, row: Row) -> "Donation": return cls(**dict(row)) -class Services(NamedTuple): +class Service(NamedTuple): id: int twitchuser: str client_id: str @@ -28,5 +28,5 @@ class Services(NamedTuple): token: Optional[int] @classmethod - def from_row(cls, row: Row) -> "Services": + def from_row(cls, row: Row) -> "Service": return cls(**dict(row)) From 9390a79ccab14b07880ca46af2b639784b7fb072 Mon Sep 17 00:00:00 2001 From: Fitti Date: Tue, 22 Jun 2021 17:06:56 +0200 Subject: [PATCH 12/40] Validate api_post_donation requests --- lnbits/extensions/twitchalerts/views_api.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/lnbits/extensions/twitchalerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py index 893b53e8..24637358 100644 --- a/lnbits/extensions/twitchalerts/views_api.py +++ b/lnbits/extensions/twitchalerts/views_api.py @@ -77,20 +77,11 @@ async def api_create_donation(): @twitchalerts_ext.route("/api/v1/postdonation", methods=["POST"]) -# @api_validate_post_request( -# schema={ -# "id": {"type": "string", "required": True}, -# "description": {"type": "string", "allow_unknown": True}, -# "onchainaddress": {"type": "string", "allow_unknown": True}, -# "payment_request": {"type": "string", "allow_unknown": True}, -# "payment_hash": {"type": "string", "allow_unknown": True}, -# "time": {"type": "integer", "allow_unknown": True}, -# "amount": {"type": "integer", "allow_unknown": True}, -# "paid": {"type": "boolean", "allow_unknown": True}, -# "timestamp": {"type": "integer", "allow_unknown": True}, -# "completelink": {"type": "string", "allow_unknown": True}, -# } -# ) +@api_validate_post_request( + schema={ + "id": {"type": "string", "required": True}, + } +) async def api_post_donation(): """Posts a paid donation to Stremalabs/StreamElements. From 8342e2bf8b51999f575d2fb26339ebab57211e1b Mon Sep 17 00:00:00 2001 From: Fitti Date: Tue, 22 Jun 2021 17:07:44 +0200 Subject: [PATCH 13/40] Return BAD_REQUEST for bad api_post_donation requests --- lnbits/extensions/twitchalerts/views_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/twitchalerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py index 24637358..ffef7d56 100644 --- a/lnbits/extensions/twitchalerts/views_api.py +++ b/lnbits/extensions/twitchalerts/views_api.py @@ -95,6 +95,6 @@ async def api_post_donation(): if await post_donation(donation_id): return "Posted!", HTTPStatus.OK else: - return "Already posted!", HTTPStatus.OK + return "Already posted!", HTTPStatus.BAD_REQUEST else: - return "Not a paid charge!", HTTPStatus.OK + return "Not a paid charge!", HTTPStatus.BAD_REQUEST From d16eae2d9d103911f96061de8c31903725634a07 Mon Sep 17 00:00:00 2001 From: Fitti Date: Tue, 22 Jun 2021 17:12:55 +0200 Subject: [PATCH 14/40] Return json instead of plain text from API --- lnbits/extensions/twitchalerts/views_api.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lnbits/extensions/twitchalerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py index ffef7d56..fb2c8e23 100644 --- a/lnbits/extensions/twitchalerts/views_api.py +++ b/lnbits/extensions/twitchalerts/views_api.py @@ -1,4 +1,4 @@ -from quart import g, redirect, request +from quart import g, redirect, request, jsonify from http import HTTPStatus from lnbits.decorators import api_validate_post_request, api_check_wallet_key @@ -93,8 +93,17 @@ async def api_post_donation(): if charge and charge.paid: print("This endpoint works!") if await post_donation(donation_id): - return "Posted!", HTTPStatus.OK + return ( + jsonify({"message": "Posted!"}), + HTTPStatus.OK + ) else: - return "Already posted!", HTTPStatus.BAD_REQUEST + return ( + jsonify({"message": "Already posted!"}), + HTTPStatus.BAD_REQUEST + ) else: - return "Not a paid charge!", HTTPStatus.BAD_REQUEST + return ( + jsonify({"message": "Not a paid charge!"}), + HTTPStatus.BAD_REQUEST + ) From 8386facbdb66bd5d35691b60a3d82b35b89f0855 Mon Sep 17 00:00:00 2001 From: Fitti Date: Tue, 22 Jun 2021 18:05:07 +0200 Subject: [PATCH 15/40] Add state for authentication --- lnbits/extensions/twitchalerts/crud.py | 7 +++- lnbits/extensions/twitchalerts/migrations.py | 1 + lnbits/extensions/twitchalerts/models.py | 1 + lnbits/extensions/twitchalerts/views_api.py | 39 +++++++++++++++++++- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/lnbits/extensions/twitchalerts/crud.py b/lnbits/extensions/twitchalerts/crud.py index 298c5680..1f750423 100644 --- a/lnbits/extensions/twitchalerts/crud.py +++ b/lnbits/extensions/twitchalerts/crud.py @@ -6,6 +6,8 @@ from ..satspay.crud import delete_charge import httpx from typing import Optional + +from lnbits.helpers import urlsafe_short_hash from lnbits.core.crud import get_wallet @@ -80,6 +82,7 @@ async def create_service( client_secret: str, wallet: str, servicename: str, + state: str = None, onchain: str = None, ) -> Service: result = await db.execute( @@ -91,9 +94,10 @@ async def create_service( wallet, servicename, authenticated, + state, onchain ) - VALUES (?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( twitchuser, @@ -102,6 +106,7 @@ async def create_service( wallet, servicename, False, + urlsafe_short_hash(), onchain, ), ) diff --git a/lnbits/extensions/twitchalerts/migrations.py b/lnbits/extensions/twitchalerts/migrations.py index 39d7f0ae..6396ded5 100644 --- a/lnbits/extensions/twitchalerts/migrations.py +++ b/lnbits/extensions/twitchalerts/migrations.py @@ -4,6 +4,7 @@ async def m001_initial(db): """ CREATE TABLE IF NOT EXISTS Services ( id INTEGER PRIMARY KEY AUTOINCREMENT, + state TEXT NOT NULL, twitchuser TEXT NOT NULL, client_id TEXT NOT NULL, client_secret TEXT NOT NULL, diff --git a/lnbits/extensions/twitchalerts/models.py b/lnbits/extensions/twitchalerts/models.py index 827b8d8d..349ba3be 100644 --- a/lnbits/extensions/twitchalerts/models.py +++ b/lnbits/extensions/twitchalerts/models.py @@ -18,6 +18,7 @@ class Donation(NamedTuple): class Service(NamedTuple): id: int + state: str twitchuser: str client_id: str client_secret: str diff --git a/lnbits/extensions/twitchalerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py index fb2c8e23..11e2823b 100644 --- a/lnbits/extensions/twitchalerts/views_api.py +++ b/lnbits/extensions/twitchalerts/views_api.py @@ -9,6 +9,7 @@ from .crud import ( create_donation, post_donation, create_service, + get_service, authenticate_service ) from ..satspay.crud import create_charge, get_charge @@ -34,11 +35,45 @@ async def api_create_service(): return redirect(redirect_url) +@twitchalerts_ext.route("/api/v1/getaccess/", methods=["GET"]) +async def api_get_access(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}" + params = { + "response_type": "code", + "client_id": service.client_id, + "client_secret": service.client_secret, + "redirect_uri": redirect_uri, + "scope": "donations.create", + "state": service.state + } + endpoint_url = "https://streamlabs.com/api/v1.0/authorize/?" + querystring = "&".join( + [f"{key}={value}" for key, value in params.items()] + ) + redirect_url = endpoint_url + querystring + return redirect(redirect_url) + else: + return ( + jsonify({"message": "Service does not exist!"}), + HTTPStatus.BAD_REQUEST + ) + + @twitchalerts_ext.route("/api/v1/authenticate/", methods=["GET"]) async def api_authenticate_service(service_id): code = request.args.get('code') - redirect_uri = request.scheme + "://" + request.headers["Host"] - redirect_uri += f"/twitchalerts/api/v1/authenticate/{service_id}" + state = request.args.get('state') + service = await get_service(service_id) + if service.state != state: + return ( + jsonify({"message": "State doesn't match!"}), + HTTPStatus.BAD_Request + ) + redirect_uri = f"/twitchalerts/api/v1/authenticate/{service_id}" url = await authenticate_service(service_id, code, redirect_uri) return redirect(url) From c754017e9dd0a4362ec8eef097b493cb4a0f78dd Mon Sep 17 00:00:00 2001 From: Fitti Date: Tue, 22 Jun 2021 18:29:01 +0200 Subject: [PATCH 16/40] Fix authentication process --- lnbits/extensions/twitchalerts/views_api.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lnbits/extensions/twitchalerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py index 11e2823b..d3330d57 100644 --- a/lnbits/extensions/twitchalerts/views_api.py +++ b/lnbits/extensions/twitchalerts/views_api.py @@ -2,6 +2,7 @@ from quart import g, redirect, request, jsonify from http import HTTPStatus from lnbits.decorators import api_validate_post_request, api_check_wallet_key +from lnbits.core.crud import get_wallet from . import twitchalerts_ext from .crud import ( @@ -30,8 +31,10 @@ from ..satspay.crud import create_charge, get_charge async def api_create_service(): """Create a service, which holds data about how/where to post donations""" service = await create_service(**g.data) + wallet = await get_wallet(service.wallet) + user = wallet.user redirect_url = request.scheme + "://" + request.headers["Host"] - redirect_url += f"/twitchalerts/?created={str(service.id)}" + redirect_url += f"/twitchalerts/?usr={user}&created={str(service.id)}" return redirect(redirect_url) @@ -45,7 +48,6 @@ async def api_get_access(service_id): params = { "response_type": "code", "client_id": service.client_id, - "client_secret": service.client_secret, "redirect_uri": redirect_uri, "scope": "donations.create", "state": service.state @@ -73,7 +75,8 @@ async def api_authenticate_service(service_id): jsonify({"message": "State doesn't match!"}), HTTPStatus.BAD_Request ) - redirect_uri = f"/twitchalerts/api/v1/authenticate/{service_id}" + redirect_uri = request.scheme + "://" + request.headers["Host"] + redirect_uri += f"/twitchalerts/api/v1/authenticate/{service_id}" url = await authenticate_service(service_id, code, redirect_uri) return redirect(url) From 1cd1a999449223b6de666c402b8e2253ba7d1a7b Mon Sep 17 00:00:00 2001 From: Fitti Date: Wed, 23 Jun 2021 09:53:10 +0200 Subject: [PATCH 17/40] Prevent brute-force token overwriting --- lnbits/extensions/twitchalerts/crud.py | 9 ++++++--- lnbits/extensions/twitchalerts/views_api.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lnbits/extensions/twitchalerts/crud.py b/lnbits/extensions/twitchalerts/crud.py index 1f750423..d4458f95 100644 --- a/lnbits/extensions/twitchalerts/crud.py +++ b/lnbits/extensions/twitchalerts/crud.py @@ -141,15 +141,18 @@ async def authenticate_service(service_id, code, redirect_uri): response = (await client.post(url, data=data)).json() print(response) token = response['access_token'] - await service_add_token(service_id, token) - return f"/twitchalerts/?usr={user}" + success = await service_add_token(service_id, token) + return f"/twitchalerts/?usr={user}", success async def service_add_token(service_id, token): + if (await get_service(service_id)).authenticated: + return False db.execute( - "UPDATE Services SET token = ? where id = ?", + "UPDATE Services SET authenticated = 1, token = ? where id = ?", (token, service_id,), ) + return True async def delete_service(service_id: int) -> None: diff --git a/lnbits/extensions/twitchalerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py index d3330d57..b529cab1 100644 --- a/lnbits/extensions/twitchalerts/views_api.py +++ b/lnbits/extensions/twitchalerts/views_api.py @@ -77,8 +77,14 @@ async def api_authenticate_service(service_id): ) redirect_uri = request.scheme + "://" + request.headers["Host"] redirect_uri += f"/twitchalerts/api/v1/authenticate/{service_id}" - url = await authenticate_service(service_id, code, redirect_uri) - return redirect(url) + url, success = await authenticate_service(service_id, code, redirect_uri) + if success: + return redirect(url) + else: + return ( + jsonify({"message": "Service already authenticated!"}), + HTTPStatus.BAD_REQUEST + ) @twitchalerts_ext.route("/api/v1/createdonation", methods=["POST"]) From 775444cd6b01366a12d1c392626a4f52b2f5ae09 Mon Sep 17 00:00:00 2001 From: Fitti Date: Thu, 24 Jun 2021 09:59:11 +0200 Subject: [PATCH 18/40] Finish donation posting API flow --- lnbits/extensions/twitchalerts/crud.py | 54 +++++++++++++++++--- lnbits/extensions/twitchalerts/migrations.py | 1 + lnbits/extensions/twitchalerts/models.py | 1 + lnbits/extensions/twitchalerts/views_api.py | 13 +---- 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/lnbits/extensions/twitchalerts/crud.py b/lnbits/extensions/twitchalerts/crud.py index d4458f95..15b05063 100644 --- a/lnbits/extensions/twitchalerts/crud.py +++ b/lnbits/extensions/twitchalerts/crud.py @@ -5,6 +5,9 @@ from ..satspay.crud import delete_charge import httpx +from http import HTTPStatus +from quart import jsonify + from typing import Optional from lnbits.helpers import urlsafe_short_hash @@ -28,11 +31,12 @@ async def get_charge_details(service_id): async def create_donation( id: str, - name: str, cur_code: str, sats: int, amount: float, service: int, + name: str = "Anonymous", + message: str = "", posted: bool = False, ) -> Donation: await db.execute( @@ -40,17 +44,19 @@ async def create_donation( INSERT INTO Donations ( id, name, + message, cur_code, sats, amount, service, posted ) - VALUES (?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( id, name, + message, cur_code, sats, amount, @@ -61,19 +67,51 @@ async def create_donation( return await get_donation(id) -async def post_donation(donation_id: str) -> bool: +async def post_donation(donation_id: str) -> tuple: donation = await get_donation(donation_id) + if not donation: + return ( + jsonify({"message": "Donation not found!"}), + HTTPStatus.BAD_REQUEST + ) if donation.posted: - return False + return ( + jsonify({"message": "Donation has already been posted!"}), + HTTPStatus.BAD_REQUEST + ) service = await get_service(donation.service) - servicename = service.servicename - if servicename == "Streamlabs": - pass + if service.servicename == "Streamlabs": + url = "https://streamlabs.com/api/v1.0/donations" + data = { + "name": donation.name, + "message": donation.message, + "identifier": "LNbits", + "amount": donation.amount, + "currency": donation.cur_code.upper(), + "access_token": service.token, + } + async with httpx.AsyncClient() as client: + response = await client.post(url, data=data) + print(response.json()) + status = [s for s in list(HTTPStatus) if s == response.status_code][0] + elif service.servicename == "StreamElements": + return ( + jsonify({"message": "StreamElements not yet supported!"}), + HTTPStatus.BAD_REQUEST + ) + else: + return ( + jsonify({"message": "Unsopported servicename"}), + HTTPStatus.BAD_REQUEST + ) await db.execute( "UPDATE Donations SET posted = 1 WHERE id = ?", (donation_id,) ) - return True + return ( + jsonify(response.json()), + status + ) async def create_service( diff --git a/lnbits/extensions/twitchalerts/migrations.py b/lnbits/extensions/twitchalerts/migrations.py index 6396ded5..6677d5d2 100644 --- a/lnbits/extensions/twitchalerts/migrations.py +++ b/lnbits/extensions/twitchalerts/migrations.py @@ -22,6 +22,7 @@ async def m001_initial(db): CREATE TABLE IF NOT EXISTS Donations ( id TEXT PRIMARY KEY, name TEXT NOT NULL, + message TEXT NOT NULL, cur_code TEXT NOT NULL, sats INT NOT NULL, amount FLOAT NOT NULL, diff --git a/lnbits/extensions/twitchalerts/models.py b/lnbits/extensions/twitchalerts/models.py index 349ba3be..32f795ff 100644 --- a/lnbits/extensions/twitchalerts/models.py +++ b/lnbits/extensions/twitchalerts/models.py @@ -5,6 +5,7 @@ from typing import NamedTuple, Optional class Donation(NamedTuple): id: str name: str + message: str cur_code: str sats: int amount: float diff --git a/lnbits/extensions/twitchalerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py index b529cab1..453a1b2f 100644 --- a/lnbits/extensions/twitchalerts/views_api.py +++ b/lnbits/extensions/twitchalerts/views_api.py @@ -133,19 +133,8 @@ async def api_post_donation(): data = await request.get_json(force=True) donation_id = data.get("id", "No ID") charge = await get_charge(donation_id) - print(charge) if charge and charge.paid: - print("This endpoint works!") - if await post_donation(donation_id): - return ( - jsonify({"message": "Posted!"}), - HTTPStatus.OK - ) - else: - return ( - jsonify({"message": "Already posted!"}), - HTTPStatus.BAD_REQUEST - ) + return await post_donation(donation_id) else: return ( jsonify({"message": "Not a paid charge!"}), From 1ed23c2635f8df432b3e623e0ea87ceb7ae1030c Mon Sep 17 00:00:00 2001 From: Fitti Date: Thu, 24 Jun 2021 10:20:38 +0200 Subject: [PATCH 19/40] Fix not awaiting get_service --- lnbits/extensions/twitchalerts/crud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/twitchalerts/crud.py b/lnbits/extensions/twitchalerts/crud.py index 15b05063..cc35c0be 100644 --- a/lnbits/extensions/twitchalerts/crud.py +++ b/lnbits/extensions/twitchalerts/crud.py @@ -19,7 +19,7 @@ async def get_charge_details(service_id): "description": f"TwitchAlerts donation for service {str(service_id)}", "time": 1440, } - service = get_service(service_id) + service = await get_service(service_id) wallet_id = service.wallet wallet = await get_wallet(wallet_id) user = wallet.user @@ -186,7 +186,7 @@ async def authenticate_service(service_id, code, redirect_uri): async def service_add_token(service_id, token): if (await get_service(service_id)).authenticated: return False - db.execute( + await db.execute( "UPDATE Services SET authenticated = 1, token = ? where id = ?", (token, service_id,), ) From 22a68f3e9f455a9914df79660081402cf87b6a30 Mon Sep 17 00:00:00 2001 From: Fitti Date: Thu, 24 Jun 2021 10:20:57 +0200 Subject: [PATCH 20/40] Redirect to correct Twitch stream --- lnbits/extensions/twitchalerts/views_api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/twitchalerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py index 453a1b2f..0bab7471 100644 --- a/lnbits/extensions/twitchalerts/views_api.py +++ b/lnbits/extensions/twitchalerts/views_api.py @@ -101,11 +101,13 @@ async def api_authenticate_service(service_id): async def api_create_donation(): """Takes data from donation form and creates+returns SatsPay charge""" webhook_base = request.scheme + "://" + request.headers["Host"] - charge_details = await get_charge_details(g.data["service"]) + service_id = g.data["service"] + service = await get_service(service_id) + charge_details = await get_charge_details(service.id) name = g.data.get("name", "Anonymous") charge = await create_charge( amount=g.data["sats"], - completelink="https://twitch.tv/Fitti", + completelink=f"https://twitch.tv/{service.twitchuser}", completelinktext="Back to Stream!", webhook=webhook_base + "/twitchalerts/api/v1/postdonation", **charge_details) From f70eac2a48888562a59acecb1e3aa4218d754bb9 Mon Sep 17 00:00:00 2001 From: Fitti Date: Mon, 28 Jun 2021 09:05:49 +0200 Subject: [PATCH 21/40] Copy and start to modify templates --- .../templates/twitchalerts/_api_docs.html | 22 + .../templates/twitchalerts/display.html | 197 ++++++++ .../templates/twitchalerts/index.html | 475 ++++++++++++++++-- 3 files changed, 653 insertions(+), 41 deletions(-) create mode 100644 lnbits/extensions/twitchalerts/templates/twitchalerts/_api_docs.html create mode 100644 lnbits/extensions/twitchalerts/templates/twitchalerts/display.html diff --git a/lnbits/extensions/twitchalerts/templates/twitchalerts/_api_docs.html b/lnbits/extensions/twitchalerts/templates/twitchalerts/_api_docs.html new file mode 100644 index 00000000..9dd12cc0 --- /dev/null +++ b/lnbits/extensions/twitchalerts/templates/twitchalerts/_api_docs.html @@ -0,0 +1,22 @@ + + + +
+ Twitch Alerts: Integrate Bitcoin into your stream alerts! +
+

+ Accept Bitcoin donations on Twitch, and integrate them into your alerts. + Present your viewers with a simple donation page, and add those donations + to Streamlabs to play alerts on your stream!
+ + Created by, Fitti +

+
+
+
diff --git a/lnbits/extensions/twitchalerts/templates/twitchalerts/display.html b/lnbits/extensions/twitchalerts/templates/twitchalerts/display.html new file mode 100644 index 00000000..20250831 --- /dev/null +++ b/lnbits/extensions/twitchalerts/templates/twitchalerts/display.html @@ -0,0 +1,197 @@ +{% extends "public.html" %} {% block page %} +
+
+ + +

{{ form_name }}

+
+
{{ form_desc }}
+
+ + + + +

{% raw %}{{amountWords}}{% endraw %}

+
+ Submit + Cancel +
+
+
+
+
+ + + + + + +
+ Copy invoice + Close +
+
+
+
+ +{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html b/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html index ac30a196..2abb6c65 100644 --- a/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html +++ b/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html @@ -1,56 +1,449 @@ {% extends "base.html" %} {% from "macros.jinja" import window_vars with context %} {% block page %} - - -
Frameworks used by LNbits
- - - {% raw %} - - - {{ tool.name }} - {{ tool.language }} - - {% endraw %} - - - -

- A magical "g" is always available, with info about the user, wallets and - extensions: -

- {% raw %}{{ g }}{% endraw %} -
-
+
+
+ + + New Service + + + + + +
+
+
Services
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + +
+
+
Donations
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+
+ + +
+ LNbits Twitch Alerts extension +
+
+ + + {% include "twitchalerts/_api_docs.html" %} + +
+
+ + + + + + + + + + +
+ Update Service + + Create Service + Cancel +
+
+
+
+
{% endblock %} {% block scripts %} {{ window_vars(user) }} From 3f353a6eaef54f6044ea18c46ee2f0cef75d2582 Mon Sep 17 00:00:00 2001 From: Fitti Date: Mon, 28 Jun 2021 09:06:13 +0200 Subject: [PATCH 22/40] Add donation getter endpoint --- lnbits/extensions/twitchalerts/crud.py | 20 ++++++++++++++++++++ lnbits/extensions/twitchalerts/views_api.py | 15 ++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lnbits/extensions/twitchalerts/crud.py b/lnbits/extensions/twitchalerts/crud.py index cc35c0be..a3d1578c 100644 --- a/lnbits/extensions/twitchalerts/crud.py +++ b/lnbits/extensions/twitchalerts/crud.py @@ -161,6 +161,14 @@ async def get_service(service_id: int) -> Optional[Service]: return Service.from_row(row) if row else None +async def get_services(wallet_id: str) -> Optional[list]: + rows = await db.fetchall( + "SELECT * FROM Services WHERE wallet = ?", + (wallet_id,) + ) + return [Service.from_row(row) for row in rows] if rows else None + + async def authenticate_service(service_id, code, redirect_uri): # The API token is passed in the querystring as 'code' service = await get_service(service_id) @@ -214,6 +222,18 @@ async def get_donation(donation_id: str) -> Optional[Donation]: return Donation.from_row(row) if row else None +async def get_donations(wallet_id: str) -> Optional[list]: + services = await get_services(wallet_id) + service_ids = [service.id for service in services] + rows = [] + for service_id in service_ids: + rows.append(await db.fetchall( + "SELECT * FROM Donations WHERE service = ?", + (service_id,) + )) + return [Donation.from_row(row) for row in rows] if rows else None + + async def delete_donation(donation_id: str) -> None: await db.execute( "DELETE FROM Donations WHERE id = ?", diff --git a/lnbits/extensions/twitchalerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py index 0bab7471..aeebd469 100644 --- a/lnbits/extensions/twitchalerts/views_api.py +++ b/lnbits/extensions/twitchalerts/views_api.py @@ -2,13 +2,14 @@ from quart import g, redirect, request, jsonify from http import HTTPStatus from lnbits.decorators import api_validate_post_request, api_check_wallet_key -from lnbits.core.crud import get_wallet +from lnbits.core.crud import get_wallet, get_user from . import twitchalerts_ext from .crud import ( get_charge_details, create_donation, post_donation, + get_donations, create_service, get_service, authenticate_service @@ -142,3 +143,15 @@ async def api_post_donation(): jsonify({"message": "Not a paid charge!"}), HTTPStatus.BAD_REQUEST ) + + +@twitchalerts_ext.route("/api/v1/donations", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_get_donations(): + wallet_ids = (await get_user(g.wallet.user)).wallet_ids + return ( + jsonify([ + donation._asdict() for donation in await get_donations(wallet_ids) + ]), + HTTPStatus.OK, + ) From 39844b257bfff28b7595769684a168ad23dbdfd6 Mon Sep 17 00:00:00 2001 From: Fitti Date: Mon, 28 Jun 2021 17:18:35 +0200 Subject: [PATCH 23/40] Further customize copied code --- lnbits/extensions/twitchalerts/crud.py | 37 ++++-- lnbits/extensions/twitchalerts/migrations.py | 1 + lnbits/extensions/twitchalerts/models.py | 1 + .../templates/twitchalerts/_api_docs.html | 39 +++---- .../templates/twitchalerts/index.html | 50 ++++---- lnbits/extensions/twitchalerts/views_api.py | 107 +++++++++++++++++- 6 files changed, 177 insertions(+), 58 deletions(-) diff --git a/lnbits/extensions/twitchalerts/crud.py b/lnbits/extensions/twitchalerts/crud.py index a3d1578c..a5a9aacf 100644 --- a/lnbits/extensions/twitchalerts/crud.py +++ b/lnbits/extensions/twitchalerts/crud.py @@ -31,6 +31,7 @@ async def get_charge_details(service_id): async def create_donation( id: str, + wallet: str, cur_code: str, sats: int, amount: float, @@ -43,6 +44,7 @@ async def create_donation( """ INSERT INTO Donations ( id, + wallet, name, message, cur_code, @@ -51,10 +53,11 @@ async def create_donation( service, posted ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( id, + wallet, name, message, cur_code, @@ -223,14 +226,10 @@ async def get_donation(donation_id: str) -> Optional[Donation]: async def get_donations(wallet_id: str) -> Optional[list]: - services = await get_services(wallet_id) - service_ids = [service.id for service in services] - rows = [] - for service_id in service_ids: - rows.append(await db.fetchall( - "SELECT * FROM Donations WHERE service = ?", - (service_id,) - )) + rows = await db.fetchall( + "SELECT * FROM Donations WHERE wallet = ?", + (wallet_id,) + ) return [Donation.from_row(row) for row in rows] if rows else None @@ -240,3 +239,23 @@ async def delete_donation(donation_id: str) -> None: (donation_id,) ) await delete_charge(donation_id) + + +async def update_donation(donation_id: str, **kwargs) -> Donation: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute(f"UPDATE form SET {q} WHERE id = ?", (*kwargs.values(), + donation_id)) + row = await db.fetchone("SELECT * FROM Donations WHERE id = ?", + (donation_id,)) + assert row, "Newly updated donation couldn't be retrieved" + return Donation(**row) + + +async def update_service(service_id: str, **kwargs) -> Donation: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute(f"UPDATE form SET {q} WHERE id = ?", (*kwargs.values(), + service_id)) + row = await db.fetchone("SELECT * FROM Services WHERE id = ?", + (service_id,)) + assert row, "Newly updated service couldn't be retrieved" + return Service(**row) diff --git a/lnbits/extensions/twitchalerts/migrations.py b/lnbits/extensions/twitchalerts/migrations.py index 6677d5d2..64d75a8d 100644 --- a/lnbits/extensions/twitchalerts/migrations.py +++ b/lnbits/extensions/twitchalerts/migrations.py @@ -21,6 +21,7 @@ async def m001_initial(db): """ CREATE TABLE IF NOT EXISTS Donations ( id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, name TEXT NOT NULL, message TEXT NOT NULL, cur_code TEXT NOT NULL, diff --git a/lnbits/extensions/twitchalerts/models.py b/lnbits/extensions/twitchalerts/models.py index 32f795ff..c1451672 100644 --- a/lnbits/extensions/twitchalerts/models.py +++ b/lnbits/extensions/twitchalerts/models.py @@ -4,6 +4,7 @@ from typing import NamedTuple, Optional class Donation(NamedTuple): id: str + wallet: str name: str message: str cur_code: str diff --git a/lnbits/extensions/twitchalerts/templates/twitchalerts/_api_docs.html b/lnbits/extensions/twitchalerts/templates/twitchalerts/_api_docs.html index 9dd12cc0..d062589f 100644 --- a/lnbits/extensions/twitchalerts/templates/twitchalerts/_api_docs.html +++ b/lnbits/extensions/twitchalerts/templates/twitchalerts/_api_docs.html @@ -1,22 +1,17 @@ - - - -
- Twitch Alerts: Integrate Bitcoin into your stream alerts! -
-

- Accept Bitcoin donations on Twitch, and integrate them into your alerts. - Present your viewers with a simple donation page, and add those donations - to Streamlabs to play alerts on your stream!
- - Created by, Fitti -

-
-
-
+ + +

+ Twitch Alerts: Integrate Bitcoin into your stream alerts! +

+

+ Accept Bitcoin donations on Twitch, and integrate them into your alerts. + Present your viewers with a simple donation page, and add those donations + to Streamlabs to play alerts on your stream!
+ For detailed setup instructions, check out + this guide!
+ + Created by, Fitti +

+
+
diff --git a/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html b/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html index 2abb6c65..b39679dd 100644 --- a/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html +++ b/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html @@ -177,31 +177,30 @@ - + > @@ -218,7 +217,7 @@ v-else unelevated color="deep-purple" - :disable="formDialog.data.clientid == null || formDialog.data.clientsecret == 0 || formDialog.data.username == null" + :disable="formDialog.data.client_id == null || formDialog.data.client_secret == 0 || formDialog.data.twitchuser == null" type="submit" >Create Service @@ -247,30 +246,37 @@ mixins: [windowMixin], data: function() { return { + servicenames: ["Streamlabs"], services: [], donations: [], servicesTable: { columns: [ {name: 'id', align: 'left', label: 'ID', field: 'id'}, - {name: 'username', align: 'left', label: 'Twitch Username', field: 'username'}, + {name: 'twitchuser', align: 'left', label: 'Twitch Username', field: 'twitchuser'}, {name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'}, { - name: 'service', + name: 'servicename', align: 'left', label: 'Service', - field: 'service' + field: 'servicename' }, { - name: 'clientid', + name: 'client_id', align: 'left', label: 'Client ID', - field: 'clientid' + field: 'client_id' }, { - name: 'clientsecret', + name: 'client_secret', align: 'left', label: 'Client Secret', - field: 'clientsecret' + field: 'client_secret' + }, + { + name: 'authenticated', + align: 'left', + label: 'Authenticated', + field: 'authenticated' } ], pagination: { @@ -310,17 +316,17 @@ }) }) }, - deleteDonation: function(ticketId) { + deleteDonation: function(donationId) { var self = this - var donations = _.findWhere(this.donations, {id: ticketId}) + var donations = _.findWhere(this.donations, {id: donationId}) LNbits.utils - .confirmDialog('Are you sure you want to delete this ticket') + .confirmDialog('Are you sure you want to delete this donation?') .onOk(function() { LNbits.api .request( 'DELETE', - '/twitchalerts/api/v1/donations/' + ticketId, + '/twitchalerts/api/v1/donations/' + donationId, _.findWhere(self.g.user.wallets, {id: donations.wallet}).inkey ) .then(function(response) { @@ -343,7 +349,7 @@ LNbits.api .request( 'GET', - '/twitchalerts/api/v1/services?all_wallets', + '/twitchalerts/api/v1/services', this.g.user.wallets[0].inkey ) .then(function(response) { diff --git a/lnbits/extensions/twitchalerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py index aeebd469..90afa7a5 100644 --- a/lnbits/extensions/twitchalerts/views_api.py +++ b/lnbits/extensions/twitchalerts/views_api.py @@ -9,15 +9,20 @@ from .crud import ( get_charge_details, create_donation, post_donation, + get_donation, get_donations, + delete_donation, create_service, get_service, - authenticate_service + get_services, + authenticate_service, + update_donation, + update_service ) from ..satspay.crud import create_charge, get_charge -@twitchalerts_ext.route("/api/v1/createservice", methods=["POST"]) +@twitchalerts_ext.route("/api/v1/services", methods=["POST"]) @api_check_wallet_key("invoice") @api_validate_post_request( schema={ @@ -88,7 +93,7 @@ async def api_authenticate_service(service_id): ) -@twitchalerts_ext.route("/api/v1/createdonation", methods=["POST"]) +@twitchalerts_ext.route("/api/v1/donations", methods=["POST"]) @api_check_wallet_key("invoice") @api_validate_post_request( schema={ @@ -114,6 +119,7 @@ async def api_create_donation(): **charge_details) await create_donation( id=charge.id, + wallet=service.wallet, name=name, cur_code=g.data["cur_code"], sats=g.data["sats"], @@ -145,13 +151,104 @@ async def api_post_donation(): ) +@twitchalerts_ext.route("/api/v1/services", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_get_services(): + wallet_ids = (await get_user(g.wallet.user)).wallet_ids + services = [] + for wallet_id in wallet_ids: + services += await get_services(wallet_id) + return ( + jsonify([ + service._asdict() for service in services + ] if services else []), + HTTPStatus.OK, + ) + + @twitchalerts_ext.route("/api/v1/donations", methods=["GET"]) @api_check_wallet_key("invoice") async def api_get_donations(): wallet_ids = (await get_user(g.wallet.user)).wallet_ids + donations = [] + for wallet_id in wallet_ids: + donations += await get_donations(wallet_id) return ( jsonify([ - donation._asdict() for donation in await get_donations(wallet_ids) - ]), + donation._asdict() for donation in donations + ] if donations else []), HTTPStatus.OK, ) + + +@twitchalerts_ext.route("/api/v1/donations/", methods=["PUT"]) +@api_check_wallet_key("invoice") +async def api_update_donation(donation_id=None): + if donation_id: + donation = await get_donation(donation_id) + + if not donation: + return ( + jsonify({"message": "Donation does not exist."}), + HTTPStatus.NOT_FOUND + ) + + if donation.wallet != g.wallet.id: + return ( + jsonify({"message": "Not your donation."}), + HTTPStatus.FORBIDDEN + ) + + donation = await update_donation(donation_id, **g.data) + else: + return ( + jsonify({"message": "No donation ID specified"}), + HTTPStatus.BAD_REQUEST + ) + return jsonify(donation._asdict()), HTTPStatus.CREATED + + +@twitchalerts_ext.route("/api/v1/services/", methods=["PUT"]) +@api_check_wallet_key("invoice") +async def api_update_service(service_id=None): + if service_id: + service = await get_service(service_id) + + if not service: + return ( + jsonify({"message": "Service does not exist."}), + HTTPStatus.NOT_FOUND + ) + + if service.wallet != g.wallet.id: + return ( + jsonify({"message": "Not your service."}), + HTTPStatus.FORBIDDEN + ) + + service = await update_service(service_id, **g.data) + else: + return ( + jsonify({"message": "No service ID specified"}), + HTTPStatus.BAD_REQUEST + ) + return jsonify(service._asdict()), HTTPStatus.CREATED + + +@twitchalerts_ext.route("/api/v1/donations/", methods=["DELETE"]) +@api_check_wallet_key("invoice") +async def api_delete_donation(donation_id): + donation = await get_donation(donation_id) + if not donation: + return ( + jsonify({"message": "No donation with this ID!"}), + HTTPStatus.NOT_FOUND + ) + if donation.wallet != g.wallet.id: + return ( + jsonify({"message": "Not authorized to delete this donation!"}), + HTTPStatus.FORBIDDEN + ) + await delete_donation(donation_id) + + return "", HTTPStatus.NO_CONTENT From d24980aa3c0a08c7e148870fe090accbac7e31ea Mon Sep 17 00:00:00 2001 From: Fitti Date: Mon, 28 Jun 2021 18:18:52 +0200 Subject: [PATCH 24/40] Small fixes + renames --- lnbits/extensions/twitchalerts/crud.py | 8 +-- .../templates/twitchalerts/index.html | 55 ++++++++++--------- lnbits/extensions/twitchalerts/views_api.py | 6 +- 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/lnbits/extensions/twitchalerts/crud.py b/lnbits/extensions/twitchalerts/crud.py index a5a9aacf..5ec1f864 100644 --- a/lnbits/extensions/twitchalerts/crud.py +++ b/lnbits/extensions/twitchalerts/crud.py @@ -243,8 +243,8 @@ async def delete_donation(donation_id: str) -> None: async def update_donation(donation_id: str, **kwargs) -> Donation: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute(f"UPDATE form SET {q} WHERE id = ?", (*kwargs.values(), - donation_id)) + await db.execute(f"UPDATE Donations SET {q} WHERE id = ?", + (*kwargs.values(), donation_id)) row = await db.fetchone("SELECT * FROM Donations WHERE id = ?", (donation_id,)) assert row, "Newly updated donation couldn't be retrieved" @@ -253,8 +253,8 @@ async def update_donation(donation_id: str, **kwargs) -> Donation: async def update_service(service_id: str, **kwargs) -> Donation: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute(f"UPDATE form SET {q} WHERE id = ?", (*kwargs.values(), - service_id)) + await db.execute(f"UPDATE Services SET {q} WHERE id = ?", + (*kwargs.values(), service_id)) row = await db.fetchone("SELECT * FROM Services WHERE id = ?", (service_id,)) assert row, "Newly updated service couldn't be retrieved" diff --git a/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html b/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html index b39679dd..cf64dfb3 100644 --- a/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html +++ b/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html @@ -4,7 +4,7 @@
- New Service @@ -62,7 +62,7 @@ flat dense size="xs" - @click="updateformDialog(props.row.id)" + @click="updateserviceDialog(props.row.id)" icon="edit" color="light-blue" > @@ -162,14 +162,14 @@
- + @@ -177,7 +177,7 @@ @@ -185,7 +185,7 @@ filled dense emit-value - v-model="formDialog.data.servicename" + v-model="serviceDialog.data.servicename" :options="servicenames" label="Streamlabs" hint="The service you use for alerts. (Currently only Streamlabs)" @@ -193,20 +193,20 @@
Create Service @@ -237,7 +237,7 @@ 'YYYY-MM-DD HH:mm' ) obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount) - obj.displayUrl = ['/twitchalerts/', obj.id].join('') + obj.displayUrl = ['/twitchalerts/api/v1/getaccess/', obj.id].join('') return obj } @@ -246,7 +246,7 @@ mixins: [windowMixin], data: function() { return { - servicenames: ["Streamlabs"], + servicenames: ['Streamlabs'], services: [], donations: [], servicesTable: { @@ -294,7 +294,7 @@ rowsPerPage: 10 } }, - formDialog: { + serviceDialog: { show: false, data: {} } @@ -360,9 +360,9 @@ }, sendServiceData: function() { var wallet = _.findWhere(this.g.user.wallets, { - id: this.formDialog.data.wallet + id: this.serviceDialog.data.wallet }) - var data = this.formDialog.data + var data = this.serviceDialog.data if (data.id) { this.updateService(wallet, data) @@ -377,22 +377,23 @@ .request('POST', '/twitchalerts/api/v1/services', wallet.inkey, data) .then(function(response) { self.services.push(mapTwitchAlerts(response.data)) - self.formDialog.show = false - self.formDialog.data = {} + self.serviceDialog.show = false + self.serviceDialog.data = {} }) .catch(function(error) { LNbits.utils.notifyApiError(error) }) }, - updateformDialog: function(formId) { - var link = _.findWhere(this.services, {id: formId}) + updateserviceDialog: function(serviceId) { + var link = _.findWhere(this.services, {id: serviceId}) console.log(link.id) - this.formDialog.data.id = link.id - this.formDialog.data.wallet = link.wallet - this.formDialog.data.name = link.name - this.formDialog.data.description = link.description - this.formDialog.data.costpword = link.costpword - this.formDialog.show = true + this.serviceDialog.data.id = link.id + this.serviceDialog.data.wallet = link.wallet + this.serviceDialog.data.twitchuser = link.twitchuser + this.serviceDialog.data.servicename = link.servicename + this.serviceDialog.data.client_id = link.client_id + this.serviceDialog.data.client_secret = link.client_secret + this.serviceDialog.show = true }, updateService: function(wallet, data) { var self = this @@ -410,8 +411,8 @@ return obj.id == data.id }) self.services.push(mapTwitchAlerts(response.data)) - self.formDialog.show = false - self.formDialog.data = {} + self.serviceDialog.show = false + self.serviceDialog.data = {} }) .catch(function(error) { LNbits.utils.notifyApiError(error) diff --git a/lnbits/extensions/twitchalerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py index 90afa7a5..25c3e277 100644 --- a/lnbits/extensions/twitchalerts/views_api.py +++ b/lnbits/extensions/twitchalerts/views_api.py @@ -157,7 +157,8 @@ async def api_get_services(): wallet_ids = (await get_user(g.wallet.user)).wallet_ids services = [] for wallet_id in wallet_ids: - services += await get_services(wallet_id) + new_services = await get_services(wallet_id) + services += new_services if new_services else [] return ( jsonify([ service._asdict() for service in services @@ -172,7 +173,8 @@ async def api_get_donations(): wallet_ids = (await get_user(g.wallet.user)).wallet_ids donations = [] for wallet_id in wallet_ids: - donations += await get_donations(wallet_id) + new_donations = await get_donations(wallet_id) + donations += new_donations if new_donations else [] return ( jsonify([ donation._asdict() for donation in donations From 841da76a8cd2834857721f8f35fefcf360455ce8 Mon Sep 17 00:00:00 2001 From: Fitti Date: Mon, 28 Jun 2021 18:50:56 +0200 Subject: [PATCH 25/40] Get working index.html --- .../templates/twitchalerts/index.html | 52 +++++-------------- lnbits/extensions/twitchalerts/views_api.py | 22 +++++++- 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html b/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html index cf64dfb3..f313c331 100644 --- a/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html +++ b/lnbits/extensions/twitchalerts/templates/twitchalerts/index.html @@ -50,6 +50,16 @@ icon="link" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" + :href="props.row.authUrl" + target="_blank" + > + @@ -57,16 +67,6 @@ {{ col.value }} - - - ", methods=["DELETE"]) +@api_check_wallet_key("invoice") +async def api_delete_service(service_id): + service = await get_service(service_id) + if not service: + return ( + jsonify({"message": "No service with this ID!"}), + HTTPStatus.NOT_FOUND + ) + if service.wallet != g.wallet.id: + return ( + jsonify({"message": "Not authorized to delete this service!"}), + HTTPStatus.FORBIDDEN + ) + await delete_service(service_id) + + return "", HTTPStatus.NO_CONTENT From d9f631d3ecedca4b3fdbae293b5e3e0e694316ad Mon Sep 17 00:00:00 2001 From: Fitti Date: Mon, 28 Jun 2021 19:02:47 +0200 Subject: [PATCH 26/40] Get donation form to load --- lnbits/extensions/twitchalerts/crud.py | 17 ++++++++++++----- .../templates/twitchalerts/display.html | 4 +--- lnbits/extensions/twitchalerts/views.py | 14 +++++++++++++- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/lnbits/extensions/twitchalerts/crud.py b/lnbits/extensions/twitchalerts/crud.py index 5ec1f864..779ac20e 100644 --- a/lnbits/extensions/twitchalerts/crud.py +++ b/lnbits/extensions/twitchalerts/crud.py @@ -156,11 +156,18 @@ async def create_service( return service -async def get_service(service_id: int) -> Optional[Service]: - row = await db.fetchone( - "SELECT * FROM Services WHERE id = ?", - (service_id,) - ) +async def get_service(service_id: int, + by_state: str = None) -> Optional[Service]: + if by_state: + row = await db.fetchone( + "SELECT * FROM Services WHERE state = ?", + (by_state,) + ) + else: + row = await db.fetchone( + "SELECT * FROM Services WHERE id = ?", + (service_id,) + ) return Service.from_row(row) if row else None diff --git a/lnbits/extensions/twitchalerts/templates/twitchalerts/display.html b/lnbits/extensions/twitchalerts/templates/twitchalerts/display.html index 20250831..71bca531 100644 --- a/lnbits/extensions/twitchalerts/templates/twitchalerts/display.html +++ b/lnbits/extensions/twitchalerts/templates/twitchalerts/display.html @@ -3,9 +3,7 @@
-

{{ form_name }}

-
-
{{ form_desc }}
+

Donate Bitcoin to {{ twitchuser }}!


") +async def donation(state): + service = await get_service(0, by_state=state) + if not service: + abort(HTTPStatus.NOT_FOUND, "Service does not exist.") + return await render_template("twitchalerts/display.html", + twitchuser=service.twitchuser, + service=service.id) From ef0dee1c5ca6fb20be6f7c86f6370d32e6818f21 Mon Sep 17 00:00:00 2001 From: Fitti Date: Mon, 28 Jun 2021 19:12:15 +0200 Subject: [PATCH 27/40] Add fiat calculation to donation --- lnbits/extensions/twitchalerts/views_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lnbits/extensions/twitchalerts/views_api.py b/lnbits/extensions/twitchalerts/views_api.py index e4230fbf..e8e32f73 100644 --- a/lnbits/extensions/twitchalerts/views_api.py +++ b/lnbits/extensions/twitchalerts/views_api.py @@ -3,6 +3,7 @@ from http import HTTPStatus from lnbits.decorators import api_validate_post_request, api_check_wallet_key from lnbits.core.crud import get_wallet, get_user +from lnbits.utils.exchange_rates import btc_price from . import twitchalerts_ext from .crud import ( @@ -107,6 +108,8 @@ async def api_authenticate_service(service_id): ) async def api_create_donation(): """Takes data from donation form and creates+returns SatsPay charge""" + price = await btc_price("USD") + amount = g.data["sats"] * (10 ** (-8)) * price webhook_base = request.scheme + "://" + request.headers["Host"] service_id = g.data["service"] service = await get_service(service_id) @@ -124,7 +127,7 @@ async def api_create_donation(): name=name, cur_code=g.data["cur_code"], sats=g.data["sats"], - amount=g.data["amount"], + amount=amount, service=g.data["service"], ) return redirect(f"/satspay/{charge.id}") From 56b85ecdac7ee40819f3b33defb55eaa23980759 Mon Sep 17 00:00:00 2001 From: Fitti Date: Mon, 28 Jun 2021 20:09:27 +0200 Subject: [PATCH 28/40] Get entire flow working --- .../templates/twitchalerts/display.html | 143 +++--------------- lnbits/extensions/twitchalerts/views_api.py | 16 +- 2 files changed, 31 insertions(+), 128 deletions(-) diff --git a/lnbits/extensions/twitchalerts/templates/twitchalerts/display.html b/lnbits/extensions/twitchalerts/templates/twitchalerts/display.html index 71bca531..ca48b0a9 100644 --- a/lnbits/extensions/twitchalerts/templates/twitchalerts/display.html +++ b/lnbits/extensions/twitchalerts/templates/twitchalerts/display.html @@ -3,79 +3,50 @@
-

Donate Bitcoin to {{ twitchuser }}!

+
Donate Bitcoin to {{ twitchuser }}

-

{% raw %}{{amountWords}}{% endraw %}

Submit - Cancel
- - - - - - -
- Copy invoice - Close -
-
-
{% endblock %} {% block scripts %}