From 7dd29b3eca2ecbd439677e56f715a8b338066301 Mon Sep 17 00:00:00 2001 From: benarc Date: Sun, 10 Oct 2021 16:23:39 +0100 Subject: [PATCH 01/75] initial --- lnbits/extensions/jukebox/README.md | 36 ++ lnbits/extensions/jukebox/__init__.py | 17 + lnbits/extensions/jukebox/config.json | 6 + lnbits/extensions/jukebox/crud.py | 122 +++++ lnbits/extensions/jukebox/migrations.py | 39 ++ lnbits/extensions/jukebox/models.py | 33 ++ lnbits/extensions/jukebox/static/js/index.js | 420 +++++++++++++++ .../extensions/jukebox/static/js/jukebox.js | 14 + lnbits/extensions/jukebox/static/spotapi.gif | Bin 0 -> 219995 bytes lnbits/extensions/jukebox/static/spotapi1.gif | Bin 0 -> 246647 bytes lnbits/extensions/jukebox/tasks.py | 28 + .../jukebox/templates/jukebox/_api_docs.html | 125 +++++ .../jukebox/templates/jukebox/error.html | 37 ++ .../jukebox/templates/jukebox/index.html | 368 +++++++++++++ .../jukebox/templates/jukebox/jukebox.html | 277 ++++++++++ lnbits/extensions/jukebox/views.py | 50 ++ lnbits/extensions/jukebox/views_api.py | 490 ++++++++++++++++++ 17 files changed, 2062 insertions(+) create mode 100644 lnbits/extensions/jukebox/README.md create mode 100644 lnbits/extensions/jukebox/__init__.py create mode 100644 lnbits/extensions/jukebox/config.json create mode 100644 lnbits/extensions/jukebox/crud.py create mode 100644 lnbits/extensions/jukebox/migrations.py create mode 100644 lnbits/extensions/jukebox/models.py create mode 100644 lnbits/extensions/jukebox/static/js/index.js create mode 100644 lnbits/extensions/jukebox/static/js/jukebox.js create mode 100644 lnbits/extensions/jukebox/static/spotapi.gif create mode 100644 lnbits/extensions/jukebox/static/spotapi1.gif create mode 100644 lnbits/extensions/jukebox/tasks.py create mode 100644 lnbits/extensions/jukebox/templates/jukebox/_api_docs.html create mode 100644 lnbits/extensions/jukebox/templates/jukebox/error.html create mode 100644 lnbits/extensions/jukebox/templates/jukebox/index.html create mode 100644 lnbits/extensions/jukebox/templates/jukebox/jukebox.html create mode 100644 lnbits/extensions/jukebox/views.py create mode 100644 lnbits/extensions/jukebox/views_api.py diff --git a/lnbits/extensions/jukebox/README.md b/lnbits/extensions/jukebox/README.md new file mode 100644 index 00000000..c761db44 --- /dev/null +++ b/lnbits/extensions/jukebox/README.md @@ -0,0 +1,36 @@ +# Jukebox + +## An actual Jukebox where users pay sats to play their favourite music from your playlists + +**Note:** To use this extension you need a Premium Spotify subscription. + +## Usage + +1. Click on "ADD SPOTIFY JUKEBOX"\ + ![add jukebox](https://i.imgur.com/NdVoKXd.png) +2. Follow the steps required on the form\ + + - give your jukebox a name + - select a wallet to receive payment + - define the price a user must pay to select a song\ + ![pick wallet price](https://i.imgur.com/4bJ8mb9.png) + - follow the steps to get your Spotify App and get the client ID and secret key\ + ![spotify keys](https://i.imgur.com/w2EzFtB.png) + - paste the codes in the form\ + ![api keys](https://i.imgur.com/6b9xauo.png) + - copy the _Redirect URL_ presented on the form\ + ![redirect url](https://i.imgur.com/GMzl0lG.png) + - on Spotify click the "EDIT SETTINGS" button and paste the copied link in the _Redirect URI's_ prompt + ![spotify app setting](https://i.imgur.com/vb0x4Tl.png) + - back on LNBits, click "AUTORIZE ACCESS" and "Agree" on the page that will open + - choose on which device the LNBits Jukebox extensions will stream to, you may have to be logged in in order to select the device (browser, smartphone app, etc...) + - and select what playlist will be available for users to choose songs (you need to have already playlist on Spotify)\ + ![select playlists](https://i.imgur.com/g4dbtED.png) + +3. After Jukebox is created, click the icon to open the dialog with the shareable QR, open the Jukebox page, etc...\ + ![shareable jukebox](https://i.imgur.com/EAh9PI0.png) +4. The users will see the Jukebox page and choose a song from the selected playlist\ + ![select song](https://i.imgur.com/YYjeQAs.png) +5. After selecting a song they'd like to hear next a dialog will show presenting the music\ + ![play for sats](https://i.imgur.com/eEHl3o8.png) +6. After payment, the song will automatically start playing on the device selected or enter the queue if some other music is already playing diff --git a/lnbits/extensions/jukebox/__init__.py b/lnbits/extensions/jukebox/__init__.py new file mode 100644 index 00000000..076ae4d9 --- /dev/null +++ b/lnbits/extensions/jukebox/__init__.py @@ -0,0 +1,17 @@ +from quart import Blueprint + +from lnbits.db import Database + +db = Database("ext_jukebox") + +jukebox_ext: Blueprint = Blueprint( + "jukebox", __name__, static_folder="static", template_folder="templates" +) + +from .views_api import * # noqa +from .views import * # noqa +from .tasks import register_listeners + +from lnbits.tasks import record_async + +jukebox_ext.record(record_async(register_listeners)) diff --git a/lnbits/extensions/jukebox/config.json b/lnbits/extensions/jukebox/config.json new file mode 100644 index 00000000..6b57bec4 --- /dev/null +++ b/lnbits/extensions/jukebox/config.json @@ -0,0 +1,6 @@ +{ + "name": "Spotify Jukebox", + "short_description": "Spotify jukebox middleware", + "icon": "radio", + "contributors": ["benarc"] +} diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py new file mode 100644 index 00000000..4e3ba2f1 --- /dev/null +++ b/lnbits/extensions/jukebox/crud.py @@ -0,0 +1,122 @@ +from typing import List, Optional + +from . import db +from .models import Jukebox, JukeboxPayment +from lnbits.helpers import urlsafe_short_hash + + +async def create_jukebox( + inkey: str, + user: str, + wallet: str, + title: str, + price: int, + sp_user: str, + sp_secret: str, + sp_access_token: Optional[str] = "", + sp_refresh_token: Optional[str] = "", + sp_device: Optional[str] = "", + sp_playlists: Optional[str] = "", +) -> Jukebox: + juke_id = urlsafe_short_hash() + result = await db.execute( + """ + INSERT INTO jukebox.jukebox (id, user, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + juke_id, + user, + title, + wallet, + sp_user, + sp_secret, + sp_access_token, + sp_refresh_token, + sp_device, + sp_playlists, + int(price), + 0, + ), + ) + jukebox = await get_jukebox(juke_id) + assert jukebox, "Newly created Jukebox couldn't be retrieved" + return jukebox + + +async def update_jukebox(juke_id: str, **kwargs) -> Optional[Jukebox]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (*kwargs.values(), juke_id) + ) + row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,)) + return Jukebox(**row) if row else None + + +async def get_jukebox(juke_id: str) -> Optional[Jukebox]: + row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,)) + return Jukebox(**row) if row else None + + +async def get_jukebox_by_user(user: str) -> Optional[Jukebox]: + row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE sp_user = ?", (user,)) + return Jukebox(**row) if row else None + + +async def get_jukeboxs(user: str) -> List[Jukebox]: + rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,)) + for row in rows: + if row.sp_playlists == "": + await delete_jukebox(row.id) + rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,)) + return [Jukebox.from_row(row) for row in rows] + + +async def delete_jukebox(juke_id: str): + await db.execute( + """ + DELETE FROM jukebox.jukebox WHERE id = ? + """, + (juke_id), + ) + + +#####################################PAYMENTS + + +async def create_jukebox_payment( + song_id: str, payment_hash: str, juke_id: str +) -> JukeboxPayment: + result = await db.execute( + """ + INSERT INTO jukebox.jukebox_payment (payment_hash, juke_id, song_id, paid) + VALUES (?, ?, ?, ?) + """, + ( + payment_hash, + juke_id, + song_id, + False, + ), + ) + jukebox_payment = await get_jukebox_payment(payment_hash) + assert jukebox_payment, "Newly created Jukebox Payment couldn't be retrieved" + return jukebox_payment + + +async def update_jukebox_payment( + payment_hash: str, **kwargs +) -> Optional[JukeboxPayment]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE jukebox.jukebox_payment SET {q} WHERE payment_hash = ?", + (*kwargs.values(), payment_hash), + ) + return await get_jukebox_payment(payment_hash) + + +async def get_jukebox_payment(payment_hash: str) -> Optional[JukeboxPayment]: + row = await db.fetchone( + "SELECT * FROM jukebox.jukebox_payment WHERE payment_hash = ?", (payment_hash,) + ) + return JukeboxPayment(**row) if row else None diff --git a/lnbits/extensions/jukebox/migrations.py b/lnbits/extensions/jukebox/migrations.py new file mode 100644 index 00000000..a0a3bd28 --- /dev/null +++ b/lnbits/extensions/jukebox/migrations.py @@ -0,0 +1,39 @@ +async def m001_initial(db): + """ + Initial jukebox table. + """ + await db.execute( + """ + CREATE TABLE jukebox.jukebox ( + id TEXT PRIMARY KEY, + "user" TEXT, + title TEXT, + wallet TEXT, + inkey TEXT, + sp_user TEXT NOT NULL, + sp_secret TEXT NOT NULL, + sp_access_token TEXT, + sp_refresh_token TEXT, + sp_device TEXT, + sp_playlists TEXT, + price INTEGER, + profit INTEGER + ); + """ + ) + + +async def m002_initial(db): + """ + Initial jukebox_payment table. + """ + await db.execute( + """ + CREATE TABLE jukebox.jukebox_payment ( + payment_hash TEXT PRIMARY KEY, + juke_id TEXT, + song_id TEXT, + paid BOOL + ); + """ + ) diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py new file mode 100644 index 00000000..03c41d67 --- /dev/null +++ b/lnbits/extensions/jukebox/models.py @@ -0,0 +1,33 @@ +from typing import NamedTuple +from sqlite3 import Row + + +class Jukebox(NamedTuple): + id: str + user: str + title: str + wallet: str + inkey: str + sp_user: str + sp_secret: str + sp_access_token: str + sp_refresh_token: str + sp_device: str + sp_playlists: str + price: int + profit: int + + @classmethod + def from_row(cls, row: Row) -> "Jukebox": + return cls(**dict(row)) + + +class JukeboxPayment(NamedTuple): + payment_hash: str + juke_id: str + song_id: str + paid: bool + + @classmethod + def from_row(cls, row: Row) -> "JukeboxPayment": + return cls(**dict(row)) diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js new file mode 100644 index 00000000..fc382d71 --- /dev/null +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -0,0 +1,420 @@ +/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ + +Vue.component(VueQrcode.name, VueQrcode) + +var mapJukebox = obj => { + obj._data = _.clone(obj) + obj.sp_id = obj.id + obj.device = obj.sp_device.split('-')[0] + playlists = obj.sp_playlists.split(',') + var i + playlistsar = [] + for (i = 0; i < playlists.length; i++) { + playlistsar.push(playlists[i].split('-')[0]) + } + obj.playlist = playlistsar.join() + return obj +} + +new Vue({ + el: '#vue', + mixins: [windowMixin], + data() { + return { + JukeboxTable: { + columns: [ + { + name: 'title', + align: 'left', + label: 'Title', + field: 'title' + }, + { + name: 'device', + align: 'left', + label: 'Device', + field: 'device' + }, + { + name: 'playlist', + align: 'left', + label: 'Playlist', + field: 'playlist' + }, + { + name: 'price', + align: 'left', + label: 'Price', + field: 'price' + } + ], + pagination: { + rowsPerPage: 10 + } + }, + isPwd: true, + tokenFetched: true, + devices: [], + filter: '', + jukebox: {}, + playlists: [], + JukeboxLinks: [], + step: 1, + locationcbPath: '', + locationcb: '', + jukeboxDialog: { + show: false, + data: {} + }, + spotifyDialog: false, + qrCodeDialog: { + show: false, + data: null + } + } + }, + computed: {}, + methods: { + openQrCodeDialog: function (linkId) { + var link = _.findWhere(this.JukeboxLinks, {id: linkId}) + + this.qrCodeDialog.data = _.clone(link) + console.log(this.qrCodeDialog.data) + this.qrCodeDialog.data.url = + window.location.protocol + '//' + window.location.host + this.qrCodeDialog.show = true + }, + getJukeboxes() { + self = this + LNbits.api + .request( + 'GET', + '/jukebox/api/v1/jukebox', + self.g.user.wallets[0].adminkey + ) + .then(function (response) { + self.JukeboxLinks = response.data.map(mapJukebox) + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }, + deleteJukebox(juke_id) { + self = this + LNbits.utils + .confirmDialog('Are you sure you want to delete this Jukebox?') + .onOk(function () { + LNbits.api + .request( + 'DELETE', + '/jukebox/api/v1/jukebox/' + juke_id, + self.g.user.wallets[0].adminkey + ) + .then(function (response) { + self.JukeboxLinks = _.reject(self.JukeboxLinks, function (obj) { + return obj.id === juke_id + }) + }) + + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }) + }, + updateJukebox: function (linkId) { + self = this + var link = _.findWhere(self.JukeboxLinks, {id: linkId}) + self.jukeboxDialog.data = _.clone(link._data) + console.log(this.jukeboxDialog.data.sp_access_token) + + self.refreshDevices() + self.refreshPlaylists() + + self.step = 4 + self.jukeboxDialog.data.sp_device = [] + self.jukeboxDialog.data.sp_playlists = [] + self.jukeboxDialog.data.sp_id = self.jukeboxDialog.data.id + self.jukeboxDialog.data.price = String(self.jukeboxDialog.data.price) + self.jukeboxDialog.show = true + }, + closeFormDialog() { + this.jukeboxDialog.data = {} + this.jukeboxDialog.show = false + this.step = 1 + }, + submitSpotifyKeys() { + self = this + self.jukeboxDialog.data.user = self.g.user.id + + LNbits.api + .request( + 'POST', + '/jukebox/api/v1/jukebox/', + self.g.user.wallets[0].adminkey, + self.jukeboxDialog.data + ) + .then(response => { + if (response.data) { + self.jukeboxDialog.data.sp_id = response.data.id + self.step = 3 + } + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }, + authAccess() { + self = this + self.requestAuthorization() + self.getSpotifyTokens() + self.$q.notify({ + spinner: true, + message: 'Processing', + timeout: 10000 + }) + }, + getSpotifyTokens() { + self = this + var counter = 0 + var timerId = setInterval(function () { + counter++ + if (!self.jukeboxDialog.data.sp_user) { + clearInterval(timerId) + } + LNbits.api + .request( + 'GET', + '/jukebox/api/v1/jukebox/' + self.jukeboxDialog.data.sp_id, + self.g.user.wallets[0].adminkey + ) + .then(response => { + if (response.data.sp_access_token) { + self.fetchAccessToken(response.data.sp_access_token) + if (self.jukeboxDialog.data.sp_access_token) { + self.refreshPlaylists() + self.refreshDevices() + console.log('this.devices') + console.log(self.devices) + console.log('this.devices') + setTimeout(function () { + if (self.devices.length < 1 || self.playlists.length < 1) { + self.$q.notify({ + spinner: true, + color: 'red', + message: + 'Error! Make sure Spotify is open on the device you wish to use, has playlists, and is playing something', + timeout: 10000 + }) + LNbits.api + .request( + 'DELETE', + '/jukebox/api/v1/jukebox/' + response.data.id, + self.g.user.wallets[0].adminkey + ) + .then(function (response) { + self.getJukeboxes() + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + clearInterval(timerId) + self.closeFormDialog() + } else { + self.step = 4 + clearInterval(timerId) + } + }, 2000) + } + } + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }, 3000) + }, + requestAuthorization() { + self = this + var url = 'https://accounts.spotify.com/authorize' + url += '?client_id=' + self.jukeboxDialog.data.sp_user + url += '&response_type=code' + url += + '&redirect_uri=' + + encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_id) + url += '&show_dialog=true' + url += + '&scope=user-read-private user-read-email user-modify-playback-state user-read-playback-position user-library-read streaming user-read-playback-state user-read-recently-played playlist-read-private' + + window.open(url) + }, + openNewDialog() { + this.jukeboxDialog.show = true + this.jukeboxDialog.data = {} + }, + createJukebox() { + self = this + self.jukeboxDialog.data.sp_playlists = self.jukeboxDialog.data.sp_playlists.join() + self.updateDB() + self.jukeboxDialog.show = false + self.getJukeboxes() + }, + updateDB() { + self = this + console.log(self.jukeboxDialog.data) + LNbits.api + .request( + 'PUT', + '/jukebox/api/v1/jukebox/' + this.jukeboxDialog.data.sp_id, + self.g.user.wallets[0].adminkey, + self.jukeboxDialog.data + ) + .then(function (response) { + console.log(response.data) + if ( + self.jukeboxDialog.data.sp_playlists && + self.jukeboxDialog.data.sp_devices + ) { + self.getJukeboxes() + // self.JukeboxLinks.push(mapJukebox(response.data)) + } + }) + }, + playlistApi(method, url, body) { + self = this + let xhr = new XMLHttpRequest() + xhr.open(method, url, true) + xhr.setRequestHeader('Content-Type', 'application/json') + xhr.setRequestHeader( + 'Authorization', + 'Bearer ' + this.jukeboxDialog.data.sp_access_token + ) + xhr.send(body) + xhr.onload = function () { + if (xhr.status == 401) { + self.refreshAccessToken() + self.playlistApi( + 'GET', + 'https://api.spotify.com/v1/me/playlists', + null + ) + } + let responseObj = JSON.parse(xhr.response) + self.jukeboxDialog.data.playlists = null + self.playlists = [] + self.jukeboxDialog.data.playlists = [] + var i + for (i = 0; i < responseObj.items.length; i++) { + self.playlists.push( + responseObj.items[i].name + '-' + responseObj.items[i].id + ) + } + console.log(self.playlists) + } + }, + refreshPlaylists() { + self = this + self.playlistApi('GET', 'https://api.spotify.com/v1/me/playlists', null) + }, + deviceApi(method, url, body) { + self = this + let xhr = new XMLHttpRequest() + xhr.open(method, url, true) + xhr.setRequestHeader('Content-Type', 'application/json') + xhr.setRequestHeader( + 'Authorization', + 'Bearer ' + this.jukeboxDialog.data.sp_access_token + ) + xhr.send(body) + xhr.onload = function () { + if (xhr.status == 401) { + self.refreshAccessToken() + self.deviceApi( + 'GET', + 'https://api.spotify.com/v1/me/player/devices', + null + ) + } + let responseObj = JSON.parse(xhr.response) + self.jukeboxDialog.data.devices = [] + + self.devices = [] + var i + for (i = 0; i < responseObj.devices.length; i++) { + self.devices.push( + responseObj.devices[i].name + '-' + responseObj.devices[i].id + ) + } + } + }, + refreshDevices() { + self = this + self.deviceApi( + 'GET', + 'https://api.spotify.com/v1/me/player/devices', + null + ) + }, + fetchAccessToken(code) { + self = this + let body = 'grant_type=authorization_code' + body += '&code=' + code + body += + '&redirect_uri=' + + encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_id) + + self.callAuthorizationApi(body) + }, + refreshAccessToken() { + self = this + let body = 'grant_type=refresh_token' + body += '&refresh_token=' + self.jukeboxDialog.data.sp_refresh_token + body += '&client_id=' + self.jukeboxDialog.data.sp_user + self.callAuthorizationApi(body) + }, + callAuthorizationApi(body) { + self = this + console.log( + btoa( + self.jukeboxDialog.data.sp_user + + ':' + + self.jukeboxDialog.data.sp_secret + ) + ) + let xhr = new XMLHttpRequest() + xhr.open('POST', 'https://accounts.spotify.com/api/token', true) + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') + xhr.setRequestHeader( + 'Authorization', + 'Basic ' + + btoa( + self.jukeboxDialog.data.sp_user + + ':' + + self.jukeboxDialog.data.sp_secret + ) + ) + xhr.send(body) + xhr.onload = function () { + let responseObj = JSON.parse(xhr.response) + if (responseObj.access_token) { + self.jukeboxDialog.data.sp_access_token = responseObj.access_token + self.jukeboxDialog.data.sp_refresh_token = responseObj.refresh_token + self.updateDB() + } + } + } + }, + created() { + console.log(this.g.user.wallets[0]) + var getJukeboxes = this.getJukeboxes + getJukeboxes() + this.selectedWallet = this.g.user.wallets[0] + this.locationcbPath = String( + [ + window.location.protocol, + '//', + window.location.host, + '/jukebox/api/v1/jukebox/spotify/cb/' + ].join('') + ) + this.locationcb = this.locationcbPath + } +}) diff --git a/lnbits/extensions/jukebox/static/js/jukebox.js b/lnbits/extensions/jukebox/static/js/jukebox.js new file mode 100644 index 00000000..ddbb2764 --- /dev/null +++ b/lnbits/extensions/jukebox/static/js/jukebox.js @@ -0,0 +1,14 @@ +/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ + +Vue.component(VueQrcode.name, VueQrcode) + +new Vue({ + el: '#vue', + mixins: [windowMixin], + data() { + return {} + }, + computed: {}, + methods: {}, + created() {} +}) diff --git a/lnbits/extensions/jukebox/static/spotapi.gif b/lnbits/extensions/jukebox/static/spotapi.gif new file mode 100644 index 0000000000000000000000000000000000000000..023efc9a9d2f387c4f20dbd055e4387902a3f7d1 GIT binary patch literal 219995 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

IL>wZxZA&Rw6XQP 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 literal 0 HcmV?d00001 diff --git a/lnbits/extensions/jukebox/static/spotapi1.gif b/lnbits/extensions/jukebox/static/spotapi1.gif new file mode 100644 index 0000000000000000000000000000000000000000..478032c56558139e89f23adb4449fec594884ec0 GIT binary patch literal 246647 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

#afAHR8@xEfy{te%>^^Qm=?n`#!! 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 Z#pBV(1fh0OekKkM>L((csf!c|YXC`rnZp18 literal 0 HcmV?d00001 diff --git a/lnbits/extensions/jukebox/tasks.py b/lnbits/extensions/jukebox/tasks.py new file mode 100644 index 00000000..65fca93d --- /dev/null +++ b/lnbits/extensions/jukebox/tasks.py @@ -0,0 +1,28 @@ +import json +import trio # type: ignore + +from lnbits.core.models import Payment +from lnbits.core.crud import create_payment +from lnbits.core import db as core_db +from lnbits.tasks import register_invoice_listener, internal_invoice_paid +from lnbits.helpers import urlsafe_short_hash + +from .crud import get_jukebox, update_jukebox_payment + + +async def register_listeners(): + invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2) + register_invoice_listener(invoice_paid_chan_send) + await wait_for_paid_invoices(invoice_paid_chan_recv) + + +async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): + async for payment in invoice_paid_chan: + await on_invoice_paid(payment) + + +async def on_invoice_paid(payment: Payment) -> None: + if "jukebox" != payment.extra.get("tag"): + # not a jukebox invoice + return + await update_jukebox_payment(payment.payment_hash, paid=True) diff --git a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html new file mode 100644 index 00000000..f5a91313 --- /dev/null +++ b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html @@ -0,0 +1,125 @@ + + To use this extension you need a Spotify client ID and client secret. You get + these by creating an app in the Spotify developers dashboard + here + +

Select the playlists you want people to be able to pay for, share + the frontend page, profit :)

+ Made by, + benarc. + Inspired by, + pirosb3. + + + + + + + GET /jukebox/api/v1/jukebox +

Headers
+ {"X-Api-Key": <admin_key>}
+
Body (application/json)
+
+ Returns 200 OK (application/json) +
+ [<jukebox_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/jukebox -H "X-Api-Key: {{ + g.user.wallets[0].adminkey }}" + + + + + + + + GET + /jukebox/api/v1/jukebox/<juke_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Body (application/json)
+
+ Returns 200 OK (application/json) +
+ <jukebox_object> +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/jukebox/<juke_id> -H + "X-Api-Key: {{ g.user.wallets[0].adminkey }}" + +
+
+
+ + + + POST/PUT + /jukebox/api/v1/jukebox/ +
Headers
+ {"X-Api-Key": <admin_key>}
+
Body (application/json)
+
+ Returns 200 OK (application/json) +
+ <jukbox_object> +
Curl example
+ curl -X POST {{ request.url_root }}api/v1/jukebox/ -d '{"user": + <string, user_id>, "title": <string>, + "wallet":<string>, "sp_user": <string, + spotify_user_account>, "sp_secret": <string, + spotify_user_secret>, "sp_access_token": <string, + not_required>, "sp_refresh_token": <string, not_required>, + "sp_device": <string, spotify_user_secret>, "sp_playlists": + <string, not_required>, "price": <integer, not_required>}' + -H "Content-type: application/json" -H "X-Api-Key: + {{g.user.wallets[0].adminkey }}" + +
+
+
+ + + + DELETE + /jukebox/api/v1/jukebox/<juke_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Body (application/json)
+
+ Returns 200 OK (application/json) +
+ <jukebox_object> +
Curl example
+ curl -X DELETE {{ request.url_root }}api/v1/jukebox/<juke_id> + -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}" + +
+
+
diff --git a/lnbits/extensions/jukebox/templates/jukebox/error.html b/lnbits/extensions/jukebox/templates/jukebox/error.html new file mode 100644 index 00000000..f6f7fd58 --- /dev/null +++ b/lnbits/extensions/jukebox/templates/jukebox/error.html @@ -0,0 +1,37 @@ +{% extends "public.html" %} {% block page %} +
+
+ + +
+

Jukebox error

+
+ + +
+ Ask the host to turn on the device and launch spotify +
+
+
+
+
+
+ + {% endblock %} {% block scripts %} + + + + {% endblock %} +
diff --git a/lnbits/extensions/jukebox/templates/jukebox/index.html b/lnbits/extensions/jukebox/templates/jukebox/index.html new file mode 100644 index 00000000..9b4efbd5 --- /dev/null +++ b/lnbits/extensions/jukebox/templates/jukebox/index.html @@ -0,0 +1,368 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + Add Spotify Jukebox + + {% raw %} + + + + + + + {% endraw %} + + +
+ +
+ + +
+ {{SITE_TITLE}} jukebox extension +
+
+ + + {% include "jukebox/_api_docs.html" %} + +
+
+ + + + + + + + + +
+
+ Continue + Continue +
+
+ Cancel +
+
+ +
+
+ + + + To use this extension you need a Spotify client ID and client secret. + You get these by creating an app in the Spotify developers dashboard + here. + + + + + + + +
+
+ Submit keys + Submit keys +
+
+ Cancel +
+
+ +
+
+ + + + In the app go to edit-settings, set the redirect URI to this link +
+ {% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw + %} Click to copy URL + +
+ Settings can be found + here. + +
+
+ Authorise access + Authorise access +
+
+ Cancel +
+
+ +
+
+ + + + +
+
+ Create Jukebox + Create Jukebox +
+
+ Cancel +
+
+
+
+
+
+ + + +
+
Shareable Jukebox QR
+
+ + + +
+ + Copy jukebox link + Open jukebox + Close +
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + + +{% endblock %} diff --git a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html new file mode 100644 index 00000000..cb3ab49d --- /dev/null +++ b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html @@ -0,0 +1,277 @@ +{% extends "public.html" %} {% block page %} {% raw %} +
+
+ + +

Currently playing

+
+
+ +
+
+ {{ currentPlay.name }}
+ {{ currentPlay.artist }} +
+
+
+
+ + + +

Pick a song

+ + +
+ + + + + + +
+
+ + + + +
+
+ +
+
+ {{ receive.name }}
+ {{ receive.artist }} +
+
+
+
+
+ Play for {% endraw %}{{ price }}{% raw %} sats + +
+
+
+ + + + + +
+ Copy invoice +
+
+
+
+{% endraw %} {% endblock %} {% block scripts %} + + + +{% endblock %} diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py new file mode 100644 index 00000000..9934ddca --- /dev/null +++ b/lnbits/extensions/jukebox/views.py @@ -0,0 +1,50 @@ +import json +import time +from datetime import datetime +from http import HTTPStatus +from lnbits.decorators import check_user_exists +from . import jukebox_ext, jukebox_renderer +from .crud import get_jukebox +from fastapi import FastAPI, Request +from fastapi.params import Depends +from fastapi.templating import Jinja2Templates +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse +from lnbits.core.models import User, Payment + + +@jukebox_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return jukebox_renderer().TemplateResponse( + "jukebox/index.html", {"request": request, "user": user.dict()} + ) + + +@jukebox_ext.get("/{juke_id}", response_class=HTMLResponse) +async def connect_to_jukebox(request: Request, juke_id): + jukebox = await get_jukebox(juke_id) + if not jukebox: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Jukebox does not exist." + ) + deviceCheck = await api_get_jukebox_device_check(juke_id) + devices = json.loads(deviceCheck[0].text) + deviceConnected = False + for device in devices["devices"]: + if device["id"] == jukebox.sp_device.split("-")[1]: + deviceConnected = True + if deviceConnected: + return jukebox_renderer().TemplateResponse( + "jukebox/display.html", + { + "request": request, + "playlists": jukebox.sp_playlists.split(","), + "juke_id": juke_id, + "price": jukebox.price, + "inkey": jukebox.inkey, + }, + ) + else: + return jukebox_renderer().TemplateResponse( + "jukebox/error.html", {"request": request} + ) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py new file mode 100644 index 00000000..71db2b8e --- /dev/null +++ b/lnbits/extensions/jukebox/views_api.py @@ -0,0 +1,490 @@ +from quart import g, jsonify, request +from http import HTTPStatus +import base64 +from lnbits.core.crud import get_wallet +from lnbits.core.services import create_invoice, check_invoice_status +import json + +from lnbits.decorators import api_check_wallet_key, api_validate_post_request +import httpx +from . import jukebox_ext +from .crud import ( + create_jukebox, + update_jukebox, + get_jukebox, + get_jukeboxs, + delete_jukebox, + create_jukebox_payment, + get_jukebox_payment, + update_jukebox_payment, +) + + +@jukebox_ext.route("/api/v1/jukebox", methods=["GET"]) +@api_check_wallet_key("admin") +async def api_get_jukeboxs(): + try: + return ( + jsonify( + [{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)] + ), + HTTPStatus.OK, + ) + except: + return "", HTTPStatus.NO_CONTENT + + +##################SPOTIFY AUTH##################### + + +@jukebox_ext.route("/api/v1/jukebox/spotify/cb/", methods=["GET"]) +async def api_check_credentials_callbac(juke_id): + sp_code = "" + sp_access_token = "" + sp_refresh_token = "" + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + if request.args.get("code"): + sp_code = request.args.get("code") + jukebox = await update_jukebox( + juke_id=juke_id, sp_secret=jukebox.sp_secret, sp_access_token=sp_code + ) + if request.args.get("access_token"): + sp_access_token = request.args.get("access_token") + sp_refresh_token = request.args.get("refresh_token") + jukebox = await update_jukebox( + juke_id=juke_id, + sp_secret=jukebox.sp_secret, + sp_access_token=sp_access_token, + sp_refresh_token=sp_refresh_token, + ) + return "

Success!

You can close this window

" + + +@jukebox_ext.route("/api/v1/jukebox/", methods=["GET"]) +@api_check_wallet_key("admin") +async def api_check_credentials_check(juke_id): + jukebox = await get_jukebox(juke_id) + return jsonify(jukebox._asdict()), HTTPStatus.CREATED + + +@jukebox_ext.route("/api/v1/jukebox/", methods=["POST"]) +@jukebox_ext.route("/api/v1/jukebox/", methods=["PUT"]) +@api_check_wallet_key("admin") +@api_validate_post_request( + schema={ + "user": {"type": "string", "empty": False, "required": True}, + "title": {"type": "string", "empty": False, "required": True}, + "wallet": {"type": "string", "empty": False, "required": True}, + "sp_user": {"type": "string", "empty": False, "required": True}, + "sp_secret": {"type": "string", "required": True}, + "sp_access_token": {"type": "string", "required": False}, + "sp_refresh_token": {"type": "string", "required": False}, + "sp_device": {"type": "string", "required": False}, + "sp_playlists": {"type": "string", "required": False}, + "price": {"type": "string", "required": False}, + } +) +async def api_create_update_jukebox(juke_id=None): + if juke_id: + jukebox = await update_jukebox(juke_id=juke_id, inkey=g.wallet.inkey, **g.data) + else: + jukebox = await create_jukebox(inkey=g.wallet.inkey, **g.data) + + return jsonify(jukebox._asdict()), HTTPStatus.CREATED + + +@jukebox_ext.route("/api/v1/jukebox/", methods=["DELETE"]) +@api_check_wallet_key("admin") +async def api_delete_item(juke_id): + await delete_jukebox(juke_id) + try: + return ( + jsonify( + [{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)] + ), + HTTPStatus.OK, + ) + except: + return "", HTTPStatus.NO_CONTENT + + +################JUKEBOX ENDPOINTS################## + +######GET ACCESS TOKEN###### + + +@jukebox_ext.route( + "/api/v1/jukebox/jb/playlist//", methods=["GET"] +) +async def api_get_jukebox_song(juke_id, sp_playlist, retry=False): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + tracks = [] + async with httpx.AsyncClient() as client: + try: + r = await client.get( + "https://api.spotify.com/v1/playlists/" + sp_playlist + "/tracks", + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + if "items" not in r.json(): + if r.status_code == 401: + token = await api_get_token(juke_id) + if token == False: + return False + elif retry: + return ( + jsonify({"error": "Failed to get auth"}), + HTTPStatus.FORBIDDEN, + ) + else: + return await api_get_jukebox_song( + juke_id, sp_playlist, retry=True + ) + return r, HTTPStatus.OK + for item in r.json()["items"]: + tracks.append( + { + "id": item["track"]["id"], + "name": item["track"]["name"], + "album": item["track"]["album"]["name"], + "artist": item["track"]["artists"][0]["name"], + "image": item["track"]["album"]["images"][0]["url"], + } + ) + except AssertionError: + something = None + return jsonify([track for track in tracks]) + + +async def api_get_token(juke_id): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + + async with httpx.AsyncClient() as client: + try: + r = await client.post( + "https://accounts.spotify.com/api/token", + timeout=40, + params={ + "grant_type": "refresh_token", + "refresh_token": jukebox.sp_refresh_token, + "client_id": jukebox.sp_user, + }, + headers={ + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": "Basic " + + base64.b64encode( + str(jukebox.sp_user + ":" + jukebox.sp_secret).encode("ascii") + ).decode("ascii"), + "Content-Type": "application/x-www-form-urlencoded", + }, + ) + if "access_token" not in r.json(): + return False + else: + await update_jukebox( + juke_id=juke_id, sp_access_token=r.json()["access_token"] + ) + except AssertionError: + something = None + return True + + +######CHECK DEVICE + + +@jukebox_ext.route("/api/v1/jukebox/jb/", methods=["GET"]) +async def api_get_jukebox_device_check(juke_id, retry=False): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + async with httpx.AsyncClient() as client: + rDevice = await client.get( + "https://api.spotify.com/v1/me/player/devices", + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + + if rDevice.status_code == 204 or rDevice.status_code == 200: + return ( + rDevice, + HTTPStatus.OK, + ) + elif rDevice.status_code == 401 or rDevice.status_code == 403: + token = await api_get_token(juke_id) + if token == False: + return ( + jsonify({"error": "No device connected"}), + HTTPStatus.FORBIDDEN, + ) + elif retry: + return ( + jsonify({"error": "Failed to get auth"}), + HTTPStatus.FORBIDDEN, + ) + else: + return api_get_jukebox_device_check(juke_id, retry=True) + else: + return ( + jsonify({"error": "No device connected"}), + HTTPStatus.FORBIDDEN, + ) + + +######GET INVOICE STUFF + + +@jukebox_ext.route("/api/v1/jukebox/jb/invoice//", methods=["GET"]) +async def api_get_jukebox_invoice(juke_id, song_id): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + try: + deviceCheck = await api_get_jukebox_device_check(juke_id) + devices = json.loads(deviceCheck[0].text) + deviceConnected = False + for device in devices["devices"]: + if device["id"] == jukebox.sp_device.split("-")[1]: + deviceConnected = True + if not deviceConnected: + return ( + jsonify({"error": "No device connected"}), + HTTPStatus.NOT_FOUND, + ) + except: + return ( + jsonify({"error": "No device connected"}), + HTTPStatus.NOT_FOUND, + ) + + invoice = await create_invoice( + wallet_id=jukebox.wallet, + amount=jukebox.price, + memo=jukebox.title, + extra={"tag": "jukebox"}, + ) + + jukebox_payment = await create_jukebox_payment(song_id, invoice[0], juke_id) + + return jsonify(invoice, jukebox_payment) + + +@jukebox_ext.route( + "/api/v1/jukebox/jb/checkinvoice//", methods=["GET"] +) +async def api_get_jukebox_invoice_check(pay_hash, juke_id): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + try: + status = await check_invoice_status(jukebox.wallet, pay_hash) + is_paid = not status.pending + except Exception as exc: + return jsonify({"paid": False}), HTTPStatus.OK + if is_paid: + wallet = await get_wallet(jukebox.wallet) + payment = await wallet.get_payment(pay_hash) + await payment.set_pending(False) + await update_jukebox_payment(pay_hash, paid=True) + return jsonify({"paid": True}), HTTPStatus.OK + return jsonify({"paid": False}), HTTPStatus.OK + + +@jukebox_ext.route( + "/api/v1/jukebox/jb/invoicep///", methods=["GET"] +) +async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + await api_get_jukebox_invoice_check(pay_hash, juke_id) + jukebox_payment = await get_jukebox_payment(pay_hash) + if jukebox_payment.paid: + async with httpx.AsyncClient() as client: + r = await client.get( + "https://api.spotify.com/v1/me/player/currently-playing?market=ES", + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + rDevice = await client.get( + "https://api.spotify.com/v1/me/player", + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + isPlaying = False + if rDevice.status_code == 200: + isPlaying = rDevice.json()["is_playing"] + + if r.status_code == 204 or isPlaying == False: + async with httpx.AsyncClient() as client: + uri = ["spotify:track:" + song_id] + r = await client.put( + "https://api.spotify.com/v1/me/player/play?device_id=" + + jukebox.sp_device.split("-")[1], + json={"uris": uri}, + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + if r.status_code == 204: + return jsonify(jukebox_payment), HTTPStatus.OK + elif r.status_code == 401 or r.status_code == 403: + token = await api_get_token(juke_id) + if token == False: + return ( + jsonify({"error": "Invoice not paid"}), + HTTPStatus.FORBIDDEN, + ) + elif retry: + return ( + jsonify({"error": "Failed to get auth"}), + HTTPStatus.FORBIDDEN, + ) + else: + return api_get_jukebox_invoice_paid( + song_id, juke_id, pay_hash, retry=True + ) + else: + return ( + jsonify({"error": "Invoice not paid"}), + HTTPStatus.FORBIDDEN, + ) + elif r.status_code == 200: + async with httpx.AsyncClient() as client: + r = await client.post( + "https://api.spotify.com/v1/me/player/queue?uri=spotify%3Atrack%3A" + + song_id + + "&device_id=" + + jukebox.sp_device.split("-")[1], + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + if r.status_code == 204: + return jsonify(jukebox_payment), HTTPStatus.OK + + elif r.status_code == 401 or r.status_code == 403: + token = await api_get_token(juke_id) + if token == False: + return ( + jsonify({"error": "Invoice not paid"}), + HTTPStatus.OK, + ) + elif retry: + return ( + jsonify({"error": "Failed to get auth"}), + HTTPStatus.FORBIDDEN, + ) + else: + return await api_get_jukebox_invoice_paid( + song_id, juke_id, pay_hash + ) + else: + return ( + jsonify({"error": "Invoice not paid"}), + HTTPStatus.OK, + ) + elif r.status_code == 401 or r.status_code == 403: + token = await api_get_token(juke_id) + if token == False: + return ( + jsonify({"error": "Invoice not paid"}), + HTTPStatus.OK, + ) + elif retry: + return ( + jsonify({"error": "Failed to get auth"}), + HTTPStatus.FORBIDDEN, + ) + else: + return await api_get_jukebox_invoice_paid( + song_id, juke_id, pay_hash + ) + return jsonify({"error": "Invoice not paid"}), HTTPStatus.OK + + +############################GET TRACKS + + +@jukebox_ext.route("/api/v1/jukebox/jb/currently/", methods=["GET"]) +async def api_get_jukebox_currently(juke_id, retry=False): + try: + jukebox = await get_jukebox(juke_id) + except: + return ( + jsonify({"error": "No Jukebox"}), + HTTPStatus.FORBIDDEN, + ) + async with httpx.AsyncClient() as client: + try: + r = await client.get( + "https://api.spotify.com/v1/me/player/currently-playing?market=ES", + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + if r.status_code == 204: + return jsonify({"error": "Nothing"}), HTTPStatus.OK + elif r.status_code == 200: + try: + response = r.json() + + track = { + "id": response["item"]["id"], + "name": response["item"]["name"], + "album": response["item"]["album"]["name"], + "artist": response["item"]["artists"][0]["name"], + "image": response["item"]["album"]["images"][0]["url"], + } + return jsonify(track), HTTPStatus.OK + except: + return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND + + elif r.status_code == 401: + token = await api_get_token(juke_id) + if token == False: + return ( + jsonify({"error": "Invoice not paid"}), + HTTPStatus.FORBIDDEN, + ) + elif retry: + return ( + jsonify({"error": "Failed to get auth"}), + HTTPStatus.FORBIDDEN, + ) + else: + return await api_get_jukebox_currently(juke_id, retry=True) + else: + return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND + except AssertionError: + return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND From 9a18720a96f483eac9db626516967e780aac2edf Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Sun, 10 Oct 2021 18:49:41 +0200 Subject: [PATCH 02/75] fix: embeddable img link in withdraw extension --- lnbits/extensions/withdraw/views.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py index 9bdaee45..eeacb36e 100644 --- a/lnbits/extensions/withdraw/views.py +++ b/lnbits/extensions/withdraw/views.py @@ -10,7 +10,7 @@ from fastapi.params import Depends from fastapi.templating import Jinja2Templates from starlette.exceptions import HTTPException -from starlette.responses import HTMLResponse +from starlette.responses import HTMLResponse, StreamingResponse from lnbits.core.models import User templates = Jinja2Templates(directory="templates") @@ -36,7 +36,7 @@ async def display(request: Request, link_id): return withdraw_renderer().TemplateResponse("withdraw/display.html", {"request":request,"link":{**link.dict(), "lnurl": link.lnurl(request)}, "unique":True}) -@withdraw_ext.get("/img/{link_id}", response_class=HTMLResponse) +@withdraw_ext.get("/img/{link_id}", response_class=StreamingResponse) async def img(request: Request, link_id): link = await get_withdraw_link(link_id, 0) if not link: @@ -50,16 +50,19 @@ async def img(request: Request, link_id): print(qr) stream = BytesIO() qr.svg(stream, scale=3) - return ( - stream.getvalue(), - 200, - { + stream.seek(0) + + async def _generator(stream: BytesIO): + yield stream.getvalue() + + return StreamingResponse( + _generator(stream), + headers={ "Content-Type": "image/svg+xml", "Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0", - }, - ) + }) @withdraw_ext.get("/print/{link_id}", response_class=HTMLResponse) From 9ff1313b0bb0d239d63cca5d29524b868948ed55 Mon Sep 17 00:00:00 2001 From: benarc Date: Sun, 10 Oct 2021 23:57:15 +0100 Subject: [PATCH 03/75] should be working --- lnbits/extensions/jukebox/__init__.py | 36 ++- lnbits/extensions/jukebox/models.py | 14 + lnbits/extensions/jukebox/tasks.py | 21 +- lnbits/extensions/jukebox/views.py | 5 +- lnbits/extensions/jukebox/views_api.py | 375 ++++++++++++++----------- lnbits/extensions/lnurlp/__init__.py | 4 +- 6 files changed, 269 insertions(+), 186 deletions(-) diff --git a/lnbits/extensions/jukebox/__init__.py b/lnbits/extensions/jukebox/__init__.py index 076ae4d9..f38b0ec7 100644 --- a/lnbits/extensions/jukebox/__init__.py +++ b/lnbits/extensions/jukebox/__init__.py @@ -1,17 +1,39 @@ -from quart import Blueprint +import asyncio + +from fastapi import APIRouter, FastAPI +from fastapi.staticfiles import StaticFiles +from starlette.routing import Mount from lnbits.db import Database +from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart db = Database("ext_jukebox") -jukebox_ext: Blueprint = Blueprint( - "jukebox", __name__, static_folder="static", template_folder="templates" -) +jukebox_static_files = [ + { + "path": "/jukebox/static", + "app": StaticFiles(directory="lnbits/extensions/jukebox/static"), + "name": "jukebox_static", + } +] + +jukebox_ext: APIRouter = APIRouter(prefix="/jukebox", tags=["jukebox"]) + + +def jukebox_renderer(): + return template_renderer( + [ + "lnbits/extensions/jukebox/templates", + ] + ) + from .views_api import * # noqa from .views import * # noqa -from .tasks import register_listeners +from .tasks import wait_for_paid_invoices -from lnbits.tasks import record_async -jukebox_ext.record(record_async(register_listeners)) +def jukebox_start(): + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py index 03c41d67..d50c830b 100644 --- a/lnbits/extensions/jukebox/models.py +++ b/lnbits/extensions/jukebox/models.py @@ -1,5 +1,19 @@ from typing import NamedTuple from sqlite3 import Row +from fastapi.param_functions import Query + + +class CreateJukeLinkData(BaseModel): + user: str = Query(None) + title: str = Query(None) + wallet: str = Query(None) + sp_user: str = Query(None) + sp_secret: str = Query(None) + sp_access_token: str = Query(None) + sp_refresh_token: str = Query(None) + sp_device: str = Query(None) + sp_playlists: str = Query(None) + price: str = Query(None) class Jukebox(NamedTuple): diff --git a/lnbits/extensions/jukebox/tasks.py b/lnbits/extensions/jukebox/tasks.py index 65fca93d..52366bea 100644 --- a/lnbits/extensions/jukebox/tasks.py +++ b/lnbits/extensions/jukebox/tasks.py @@ -1,23 +1,20 @@ +import asyncio import json -import trio # type: ignore +import httpx -from lnbits.core.models import Payment -from lnbits.core.crud import create_payment from lnbits.core import db as core_db -from lnbits.tasks import register_invoice_listener, internal_invoice_paid -from lnbits.helpers import urlsafe_short_hash +from lnbits.core.models import Payment +from lnbits.tasks import register_invoice_listener from .crud import get_jukebox, update_jukebox_payment -async def register_listeners(): - invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2) - register_invoice_listener(invoice_paid_chan_send) - await wait_for_paid_invoices(invoice_paid_chan_recv) +async def wait_for_paid_invoices(): + invoice_queue = asyncio.Queue() + register_invoice_listener(invoice_queue) - -async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): - async for payment in invoice_paid_chan: + while True: + payment = await invoice_queue.get() await on_invoice_paid(payment) diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py index 9934ddca..360f75e3 100644 --- a/lnbits/extensions/jukebox/views.py +++ b/lnbits/extensions/jukebox/views.py @@ -2,7 +2,7 @@ import json import time from datetime import datetime from http import HTTPStatus -from lnbits.decorators import check_user_exists +from lnbits.decorators import check_user_exists, WalletTypeInfo, get_key_type from . import jukebox_ext, jukebox_renderer from .crud import get_jukebox from fastapi import FastAPI, Request @@ -11,6 +11,9 @@ from fastapi.templating import Jinja2Templates from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse from lnbits.core.models import User, Payment +from .views_api import api_get_jukebox_device_check + +templates = Jinja2Templates(directory="templates") @jukebox_ext.get("/", response_class=HTMLResponse) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 71db2b8e..44575c6b 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -1,11 +1,23 @@ -from quart import g, jsonify, request +from fastapi import Request from http import HTTPStatus +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse, JSONResponse # type: ignore import base64 from lnbits.core.crud import get_wallet from lnbits.core.services import create_invoice, check_invoice_status import json - -from lnbits.decorators import api_check_wallet_key, api_validate_post_request +from typing import Optional +from fastapi.params import Depends +from fastapi.param_functions import Query +from pydantic.main import BaseModel +from .models import CreateJukeLinkData +from lnbits.decorators import ( + check_user_exists, + WalletTypeInfo, + get_key_type, + api_check_wallet_key, + api_validate_post_request, +) import httpx from . import jukebox_ext from .crud import ( @@ -20,43 +32,52 @@ from .crud import ( ) -@jukebox_ext.route("/api/v1/jukebox", methods=["GET"]) -@api_check_wallet_key("admin") -async def api_get_jukeboxs(): +@jukebox_ext.get("/api/v1/jukebox", status_code=HTTPStatus.OK) +async def api_get_jukeboxs( + req: Request, + wallet: WalletTypeInfo = Depends(get_key_type), + all_wallets: bool = Query(False), +): + wallet_user = wallet.wallet[0].user + try: - return ( - jsonify( - [{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)] - ), - HTTPStatus.OK, - ) + return [ + {**jukebox.dict(), "jukebox": jukebox.jukebox(req)} + for jukebox in await get_jukeboxs(wallet_user) + ] + except: - return "", HTTPStatus.NO_CONTENT + raise HTTPException( + status_code=HTTPStatus.NO_CONTENT, + detail="No Jukeboxes", + ) ##################SPOTIFY AUTH##################### -@jukebox_ext.route("/api/v1/jukebox/spotify/cb/", methods=["GET"]) -async def api_check_credentials_callbac(juke_id): +@jukebox_ext.get("/api/v1/jukebox/spotify/cb/", status_code=HTTPStatus.OK) +async def api_check_credentials_callbac( + juke_id: str = Query(None), + code: str = Query(None), + access_token: str = Query(None), + refresh_token: str = Query(None), +): sp_code = "" sp_access_token = "" sp_refresh_token = "" try: jukebox = await get_jukebox(juke_id) except: - return ( - jsonify({"error": "No Jukebox"}), - HTTPStatus.FORBIDDEN, - ) - if request.args.get("code"): - sp_code = request.args.get("code") + raise HTTPException(detail="No Jukebox", status_code=HTTPStatus.FORBIDDEN) + if code: + sp_code = code jukebox = await update_jukebox( juke_id=juke_id, sp_secret=jukebox.sp_secret, sp_access_token=sp_code ) - if request.args.get("access_token"): - sp_access_token = request.args.get("access_token") - sp_refresh_token = request.args.get("refresh_token") + if access_token: + sp_access_token = access_token + sp_refresh_token = refresh_token jukebox = await update_jukebox( juke_id=juke_id, sp_secret=jukebox.sp_secret, @@ -66,52 +87,42 @@ async def api_check_credentials_callbac(juke_id): return "

Success!

You can close this window

" -@jukebox_ext.route("/api/v1/jukebox/", methods=["GET"]) -@api_check_wallet_key("admin") -async def api_check_credentials_check(juke_id): +@jukebox_ext.get("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK) +async def api_check_credentials_check( + juke_id=None, wallet: WalletTypeInfo = Depends(get_key_type) +): jukebox = await get_jukebox(juke_id) - return jsonify(jukebox._asdict()), HTTPStatus.CREATED + return jukebox._asdict() -@jukebox_ext.route("/api/v1/jukebox/", methods=["POST"]) -@jukebox_ext.route("/api/v1/jukebox/", methods=["PUT"]) -@api_check_wallet_key("admin") -@api_validate_post_request( - schema={ - "user": {"type": "string", "empty": False, "required": True}, - "title": {"type": "string", "empty": False, "required": True}, - "wallet": {"type": "string", "empty": False, "required": True}, - "sp_user": {"type": "string", "empty": False, "required": True}, - "sp_secret": {"type": "string", "required": True}, - "sp_access_token": {"type": "string", "required": False}, - "sp_refresh_token": {"type": "string", "required": False}, - "sp_device": {"type": "string", "required": False}, - "sp_playlists": {"type": "string", "required": False}, - "price": {"type": "string", "required": False}, - } -) -async def api_create_update_jukebox(juke_id=None): +@jukebox_ext.post("/api/v1/jukebox", status_code=HTTPStatus.CREATED) +@jukebox_ext.put("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK) +async def api_create_update_jukebox( + data: CreateJukeLinkData, + juke_id=None, + wallet: WalletTypeInfo = Depends(get_key_type), +): if juke_id: jukebox = await update_jukebox(juke_id=juke_id, inkey=g.wallet.inkey, **g.data) else: jukebox = await create_jukebox(inkey=g.wallet.inkey, **g.data) - return jsonify(jukebox._asdict()), HTTPStatus.CREATED + return jukebox._asdict() -@jukebox_ext.route("/api/v1/jukebox/", methods=["DELETE"]) -@api_check_wallet_key("admin") -async def api_delete_item(juke_id): +@jukebox_ext.delete("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK) +async def api_delete_item( + juke_id=None, + wallet: WalletTypeInfo = Depends(get_key_type), +): await delete_jukebox(juke_id) try: - return ( - jsonify( - [{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)] - ), - HTTPStatus.OK, - ) + return [{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)] except: - return "", HTTPStatus.NO_CONTENT + raise HTTPException( + status_code=HTTPStatus.NO_CONTENT, + detail="No Jukebox", + ) ################JUKEBOX ENDPOINTS################## @@ -119,16 +130,20 @@ async def api_delete_item(juke_id): ######GET ACCESS TOKEN###### -@jukebox_ext.route( - "/api/v1/jukebox/jb/playlist//", methods=["GET"] +@jukebox_ext.get( + "/api/v1/jukebox/jb/playlist/{juke_id}/{sp_playlist}", status_code=HTTPStatus.OK ) -async def api_get_jukebox_song(juke_id, sp_playlist, retry=False): +async def api_get_jukebox_song( + juke_id: str = Query(None), + sp_playlist: str = Query(None), + retry: bool = Query(False), +): try: jukebox = await get_jukebox(juke_id) except: - return ( - jsonify({"error": "No Jukebox"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="No Jukeboxes", ) tracks = [] async with httpx.AsyncClient() as client: @@ -144,15 +159,15 @@ async def api_get_jukebox_song(juke_id, sp_playlist, retry=False): if token == False: return False elif retry: - return ( - jsonify({"error": "Failed to get auth"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Failed to get auth", ) else: return await api_get_jukebox_song( juke_id, sp_playlist, retry=True ) - return r, HTTPStatus.OK + return r for item in r.json()["items"]: tracks.append( { @@ -163,18 +178,18 @@ async def api_get_jukebox_song(juke_id, sp_playlist, retry=False): "image": item["track"]["album"]["images"][0]["url"], } ) - except AssertionError: + except: something = None - return jsonify([track for track in tracks]) + return [track for track in tracks] -async def api_get_token(juke_id): +async def api_get_token(juke_id=None): try: jukebox = await get_jukebox(juke_id) except: - return ( - jsonify({"error": "No Jukebox"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="No Jukeboxes", ) async with httpx.AsyncClient() as client: @@ -202,7 +217,7 @@ async def api_get_token(juke_id): await update_jukebox( juke_id=juke_id, sp_access_token=r.json()["access_token"] ) - except AssertionError: + except: something = None return True @@ -210,14 +225,17 @@ async def api_get_token(juke_id): ######CHECK DEVICE -@jukebox_ext.route("/api/v1/jukebox/jb/", methods=["GET"]) -async def api_get_jukebox_device_check(juke_id, retry=False): +@jukebox_ext.get("/api/v1/jukebox/jb/{juke_id}", status_code=HTTPStatus.OK) +async def api_get_jukebox_device_check( + juke_id: str = Query(None), + retry: bool = Query(False), +): try: jukebox = await get_jukebox(juke_id) except: - return ( - jsonify({"error": "No Jukebox"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="No Jukeboxes", ) async with httpx.AsyncClient() as client: rDevice = await client.get( @@ -227,42 +245,44 @@ async def api_get_jukebox_device_check(juke_id, retry=False): ) if rDevice.status_code == 204 or rDevice.status_code == 200: - return ( - rDevice, - HTTPStatus.OK, - ) + return rDevice elif rDevice.status_code == 401 or rDevice.status_code == 403: token = await api_get_token(juke_id) if token == False: - return ( - jsonify({"error": "No device connected"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="No devices connected", ) elif retry: - return ( - jsonify({"error": "Failed to get auth"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Failed to get auth", ) else: return api_get_jukebox_device_check(juke_id, retry=True) else: - return ( - jsonify({"error": "No device connected"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="No device connected", ) ######GET INVOICE STUFF -@jukebox_ext.route("/api/v1/jukebox/jb/invoice//", methods=["GET"]) -async def api_get_jukebox_invoice(juke_id, song_id): +@jukebox_ext.get( + "/api/v1/jukebox/jb/invoice/{juke_id}/{song_id}", status_code=HTTPStatus.OK +) +async def api_get_jukebox_invoice( + juke_id: str = Query(None), + song_id: str = Query(None), +): try: jukebox = await get_jukebox(juke_id) except: - return ( - jsonify({"error": "No Jukebox"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="No jukebox", ) try: deviceCheck = await api_get_jukebox_device_check(juke_id) @@ -272,14 +292,14 @@ async def api_get_jukebox_invoice(juke_id, song_id): if device["id"] == jukebox.sp_device.split("-")[1]: deviceConnected = True if not deviceConnected: - return ( - jsonify({"error": "No device connected"}), - HTTPStatus.NOT_FOUND, + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="No device connected", ) except: - return ( - jsonify({"error": "No device connected"}), - HTTPStatus.NOT_FOUND, + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="No device connected", ) invoice = await create_invoice( @@ -291,44 +311,53 @@ async def api_get_jukebox_invoice(juke_id, song_id): jukebox_payment = await create_jukebox_payment(song_id, invoice[0], juke_id) - return jsonify(invoice, jukebox_payment) + return {invoice, jukebox_payment} -@jukebox_ext.route( - "/api/v1/jukebox/jb/checkinvoice//", methods=["GET"] +@jukebox_ext.get( + "/api/v1/jukebox/jb/checkinvoice/{pay_hash}/{juke_id}", status_code=HTTPStatus.OK ) -async def api_get_jukebox_invoice_check(pay_hash, juke_id): +async def api_get_jukebox_invoice_check( + pay_hash: str = Query(None), + juke_id: str = Query(None), +): try: jukebox = await get_jukebox(juke_id) except: - return ( - jsonify({"error": "No Jukebox"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="No jukebox", ) try: status = await check_invoice_status(jukebox.wallet, pay_hash) is_paid = not status.pending - except Exception as exc: - return jsonify({"paid": False}), HTTPStatus.OK + except: + return {"paid": False} if is_paid: wallet = await get_wallet(jukebox.wallet) payment = await wallet.get_payment(pay_hash) await payment.set_pending(False) await update_jukebox_payment(pay_hash, paid=True) - return jsonify({"paid": True}), HTTPStatus.OK - return jsonify({"paid": False}), HTTPStatus.OK + return {"paid": True} + return {"paid": False} -@jukebox_ext.route( - "/api/v1/jukebox/jb/invoicep///", methods=["GET"] +@jukebox_ext.get( + "/api/v1/jukebox/jb/invoicep/{song_id}/{juke_id}/{pay_hash}", + status_code=HTTPStatus.OK, ) -async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False): +async def api_get_jukebox_invoice_paid( + song_id: str = Query(None), + juke_id: str = Query(None), + pay_hash: str = Query(None), + retry: bool = Query(False), +): try: jukebox = await get_jukebox(juke_id) except: - return ( - jsonify({"error": "No Jukebox"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="No jukebox", ) await api_get_jukebox_invoice_check(pay_hash, juke_id) jukebox_payment = await get_jukebox_payment(pay_hash) @@ -363,23 +392,23 @@ async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False): elif r.status_code == 401 or r.status_code == 403: token = await api_get_token(juke_id) if token == False: - return ( - jsonify({"error": "Invoice not paid"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Invoice not paid", ) elif retry: - return ( - jsonify({"error": "Failed to get auth"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Failed to get auth", ) else: return api_get_jukebox_invoice_paid( song_id, juke_id, pay_hash, retry=True ) else: - return ( - jsonify({"error": "Invoice not paid"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Invoice not paid", ) elif r.status_code == 200: async with httpx.AsyncClient() as client: @@ -392,59 +421,65 @@ async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False): headers={"Authorization": "Bearer " + jukebox.sp_access_token}, ) if r.status_code == 204: - return jsonify(jukebox_payment), HTTPStatus.OK + return jukebox_payment elif r.status_code == 401 or r.status_code == 403: token = await api_get_token(juke_id) if token == False: - return ( - jsonify({"error": "Invoice not paid"}), - HTTPStatus.OK, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Invoice not paid", ) elif retry: - return ( - jsonify({"error": "Failed to get auth"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Failed to get auth", ) else: return await api_get_jukebox_invoice_paid( song_id, juke_id, pay_hash ) else: - return ( - jsonify({"error": "Invoice not paid"}), - HTTPStatus.OK, + raise HTTPException( + status_code=HTTPStatus.OK, + detail="Invoice not paid", ) elif r.status_code == 401 or r.status_code == 403: token = await api_get_token(juke_id) if token == False: - return ( - jsonify({"error": "Invoice not paid"}), - HTTPStatus.OK, + raise HTTPException( + status_code=HTTPStatus.OK, + detail="Invoice not paid", ) elif retry: - return ( - jsonify({"error": "Failed to get auth"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Failed to get auth", ) else: return await api_get_jukebox_invoice_paid( song_id, juke_id, pay_hash ) - return jsonify({"error": "Invoice not paid"}), HTTPStatus.OK + raise HTTPException( + status_code=HTTPStatus.OK, + detail="Invoice not paid", + ) ############################GET TRACKS -@jukebox_ext.route("/api/v1/jukebox/jb/currently/", methods=["GET"]) -async def api_get_jukebox_currently(juke_id, retry=False): +@jukebox_ext.get("/api/v1/jukebox/jb/currently/{juke_id}", status_code=HTTPStatus.OK) +async def api_get_jukebox_currently( + retry: bool = Query(False), + juke_id: str = Query(None), +): try: jukebox = await get_jukebox(juke_id) except: - return ( - jsonify({"error": "No Jukebox"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="No jukebox", ) async with httpx.AsyncClient() as client: try: @@ -454,7 +489,10 @@ async def api_get_jukebox_currently(juke_id, retry=False): headers={"Authorization": "Bearer " + jukebox.sp_access_token}, ) if r.status_code == 204: - return jsonify({"error": "Nothing"}), HTTPStatus.OK + raise HTTPException( + status_code=HTTPStatus.OK, + detail="Nothing", + ) elif r.status_code == 200: try: response = r.json() @@ -466,25 +504,34 @@ async def api_get_jukebox_currently(juke_id, retry=False): "artist": response["item"]["artists"][0]["name"], "image": response["item"]["album"]["images"][0]["url"], } - return jsonify(track), HTTPStatus.OK + return track except: - return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Something went wrong", + ) elif r.status_code == 401: token = await api_get_token(juke_id) if token == False: - return ( - jsonify({"error": "Invoice not paid"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="INvoice not paid", ) elif retry: - return ( - jsonify({"error": "Failed to get auth"}), - HTTPStatus.FORBIDDEN, + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Failed to get auth", ) else: - return await api_get_jukebox_currently(juke_id, retry=True) + return await api_get_jukebox_currently(retry=True, juke_id=juke_id) else: - return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND - except AssertionError: - return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Something went wrong", + ) + except: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Something went wrong", + ) diff --git a/lnbits/extensions/lnurlp/__init__.py b/lnbits/extensions/lnurlp/__init__.py index af12d57f..b163dfb7 100644 --- a/lnbits/extensions/lnurlp/__init__.py +++ b/lnbits/extensions/lnurlp/__init__.py @@ -24,6 +24,7 @@ lnurlp_ext: APIRouter = APIRouter( # "lnurlp", __name__, static_folder="static", template_folder="templates" ) + def lnurlp_renderer(): return template_renderer( [ @@ -37,13 +38,12 @@ from .views import * # noqa from .tasks import wait_for_paid_invoices from .lnurl import * # noqa + def lnurlp_start(): loop = asyncio.get_event_loop() loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) - - # from lnbits.tasks import record_async # lnurlp_ext.record(record_async(register_listeners)) From de97c0f696f28bab040d5d6d966586524b818f99 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Mon, 11 Oct 2021 11:46:36 +0100 Subject: [PATCH 04/75] lnurlp import execption withdraw not working --- lnbits/extensions/lnurlp/lnurl.py | 1 + lnbits/extensions/withdraw/crud.py | 2 +- lnbits/extensions/withdraw/lnurl.py | 10 +++++----- lnbits/extensions/withdraw/models.py | 2 +- .../withdraw/templates/withdraw/display.html | 6 +++--- lnbits/extensions/withdraw/views.py | 3 ++- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lnbits/extensions/lnurlp/lnurl.py b/lnbits/extensions/lnurlp/lnurl.py index ca17646f..fc6cc545 100644 --- a/lnbits/extensions/lnurlp/lnurl.py +++ b/lnbits/extensions/lnurlp/lnurl.py @@ -2,6 +2,7 @@ import hashlib import math from http import HTTPStatus from fastapi import FastAPI, Request +from starlette.exceptions import HTTPException from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore from lnbits.core.services import create_invoice diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py index 183d8629..839e7a40 100644 --- a/lnbits/extensions/withdraw/crud.py +++ b/lnbits/extensions/withdraw/crud.py @@ -61,7 +61,7 @@ async def get_withdraw_link(link_id: str, num=0) -> Optional[WithdrawLink]: # for item in row: # link.append(item) # link.append(num) - print("GET_LINK", WithdrawLink.from_row(row)) + # print("GET_LINK", WithdrawLink.from_row(row)) return WithdrawLink.from_row(row) diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py index 75339cf7..dadc52e0 100644 --- a/lnbits/extensions/withdraw/lnurl.py +++ b/lnbits/extensions/withdraw/lnurl.py @@ -3,7 +3,9 @@ from http import HTTPStatus from datetime import datetime from lnbits.core.services import pay_invoice +from fastapi.param_functions import Query from starlette.requests import Request +from starlette.exceptions import HTTPException from . import withdraw_ext from .crud import get_withdraw_link_by_hash, update_withdraw_link @@ -80,19 +82,17 @@ async def api_lnurl_multi_response(request: Request, unique_hash, id_unique_hash # HTTPStatus.OK, # ) - return link.lnurl_response(request).dict() + return link.lnurl_response(req=request).dict() # CALLBACK @withdraw_ext.get("/api/v1/lnurl/cb/{unique_hash}", status_code=HTTPStatus.OK, name="withdraw.api_lnurl_callback") -async def api_lnurl_callback(unique_hash): +async def api_lnurl_callback(unique_hash, k1: str = Query(...), pr: str = Query(...)): link = await get_withdraw_link_by_hash(unique_hash) - k1 = request.query_params['k1'] - payment_request = request.query_params['pr'] + payment_request = pr now = int(datetime.now().timestamp()) - if not link: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py index 55b234e3..b01ad654 100644 --- a/lnbits/extensions/withdraw/models.py +++ b/lnbits/extensions/withdraw/models.py @@ -62,7 +62,7 @@ class WithdrawLink(BaseModel): def lnurl_response(self, req: Request) -> LnurlWithdrawResponse: url = req.url_for( - "withdraw.api_lnurl_callback", unique_hash=self.unique_hash, _external=True + name="withdraw.api_lnurl_callback", unique_hash=self.unique_hash ) return LnurlWithdrawResponse( callback=url, diff --git a/lnbits/extensions/withdraw/templates/withdraw/display.html b/lnbits/extensions/withdraw/templates/withdraw/display.html index f4d6ef9d..245b3ed1 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/display.html +++ b/lnbits/extensions/withdraw/templates/withdraw/display.html @@ -7,10 +7,10 @@ {% if link.is_spent %} Withdraw is spent. {% endif %} - + @@ -18,7 +18,7 @@
- Copy LNURL
diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py index eeacb36e..8d6eeee2 100644 --- a/lnbits/extensions/withdraw/views.py +++ b/lnbits/extensions/withdraw/views.py @@ -33,7 +33,8 @@ async def display(request: Request, link_id): ) # response.status_code = HTTPStatus.NOT_FOUND # return "Withdraw link does not exist." #probably here is where we should return the 404?? - return withdraw_renderer().TemplateResponse("withdraw/display.html", {"request":request,"link":{**link.dict(), "lnurl": link.lnurl(request)}, "unique":True}) + print("LINK", link) + return withdraw_renderer().TemplateResponse("withdraw/display.html", {"request":request,"link":link.dict(), "lnurl": link.lnurl(req=request), "unique":True}) @withdraw_ext.get("/img/{link_id}", response_class=StreamingResponse) From 706d5843321eb80cb62bb20f62634368e3c669ee Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Mon, 11 Oct 2021 11:52:21 +0100 Subject: [PATCH 05/75] added usermanager extension --- lnbits/extensions/usermanager/README.md | 26 + lnbits/extensions/usermanager/__init__.py | 12 + lnbits/extensions/usermanager/config.json | 6 + lnbits/extensions/usermanager/crud.py | 122 +++++ lnbits/extensions/usermanager/migrations.py | 31 ++ lnbits/extensions/usermanager/models.py | 23 + .../templates/usermanager/_api_docs.html | 259 ++++++++++ .../templates/usermanager/index.html | 473 ++++++++++++++++++ lnbits/extensions/usermanager/views.py | 12 + lnbits/extensions/usermanager/views_api.py | 156 ++++++ 10 files changed, 1120 insertions(+) create mode 100644 lnbits/extensions/usermanager/README.md create mode 100644 lnbits/extensions/usermanager/__init__.py create mode 100644 lnbits/extensions/usermanager/config.json create mode 100644 lnbits/extensions/usermanager/crud.py create mode 100644 lnbits/extensions/usermanager/migrations.py create mode 100644 lnbits/extensions/usermanager/models.py create mode 100644 lnbits/extensions/usermanager/templates/usermanager/_api_docs.html create mode 100644 lnbits/extensions/usermanager/templates/usermanager/index.html create mode 100644 lnbits/extensions/usermanager/views.py create mode 100644 lnbits/extensions/usermanager/views_api.py diff --git a/lnbits/extensions/usermanager/README.md b/lnbits/extensions/usermanager/README.md new file mode 100644 index 00000000..b6f30627 --- /dev/null +++ b/lnbits/extensions/usermanager/README.md @@ -0,0 +1,26 @@ +# User Manager + +## Make and manage users/wallets + +To help developers use LNbits to manage their users, the User Manager extension allows the creation and management of users and wallets. + +For example, a games developer may be developing a game that needs each user to have their own wallet, LNbits can be included in the developers stack as the user and wallet manager. Or someone wanting to manage their family's wallets (wife, children, parents, etc...) or you want to host a community Lightning Network node and want to manage wallets for the users. + +## Usage + +1. Click the button "NEW USER" to create a new user\ + ![new user](https://i.imgur.com/4yZyfJE.png) +2. Fill the user information\ + - username + - the generated wallet name, user can create other wallets later on + - email + - set a password + ![user information](https://i.imgur.com/40du7W5.png) +3. After creating your user, it will appear in the **Users** section, and a user's wallet in the **Wallets** section. +4. Next you can share the wallet with the corresponding user\ + ![user wallet](https://i.imgur.com/gAyajbx.png) +5. If you need to create more wallets for some user, click "NEW WALLET" at the top\ + ![multiple wallets](https://i.imgur.com/wovVnim.png) + - select the existing user you wish to add the wallet + - set a wallet name\ + ![new wallet](https://i.imgur.com/sGwG8dC.png) diff --git a/lnbits/extensions/usermanager/__init__.py b/lnbits/extensions/usermanager/__init__.py new file mode 100644 index 00000000..53154812 --- /dev/null +++ b/lnbits/extensions/usermanager/__init__.py @@ -0,0 +1,12 @@ +from quart import Blueprint +from lnbits.db import Database + +db = Database("ext_usermanager") + +usermanager_ext: Blueprint = Blueprint( + "usermanager", __name__, static_folder="static", template_folder="templates" +) + + +from .views_api import * # noqa +from .views import * # noqa diff --git a/lnbits/extensions/usermanager/config.json b/lnbits/extensions/usermanager/config.json new file mode 100644 index 00000000..7391ec29 --- /dev/null +++ b/lnbits/extensions/usermanager/config.json @@ -0,0 +1,6 @@ +{ + "name": "User Manager", + "short_description": "Generate users and wallets", + "icon": "person_add", + "contributors": ["benarc"] +} diff --git a/lnbits/extensions/usermanager/crud.py b/lnbits/extensions/usermanager/crud.py new file mode 100644 index 00000000..a7854ad8 --- /dev/null +++ b/lnbits/extensions/usermanager/crud.py @@ -0,0 +1,122 @@ +from typing import Optional, List + +from lnbits.core.models import Payment +from lnbits.core.crud import ( + create_account, + get_user, + get_payments, + create_wallet, + delete_wallet, +) + +from . import db +from .models import Users, Wallets + + +### Users + + +async def create_usermanager_user( + user_name: str, + wallet_name: str, + admin_id: str, + email: Optional[str] = None, + password: Optional[str] = None, +) -> Users: + account = await create_account() + user = await get_user(account.id) + assert user, "Newly created user couldn't be retrieved" + + wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name) + + await db.execute( + """ + INSERT INTO usermanager.users (id, name, admin, email, password) + VALUES (?, ?, ?, ?, ?) + """, + (user.id, user_name, admin_id, email, password), + ) + + await db.execute( + """ + INSERT INTO usermanager.wallets (id, admin, name, "user", adminkey, inkey) + VALUES (?, ?, ?, ?, ?, ?) + """, + (wallet.id, admin_id, wallet_name, user.id, wallet.adminkey, wallet.inkey), + ) + + user_created = await get_usermanager_user(user.id) + assert user_created, "Newly created user couldn't be retrieved" + return user_created + + +async def get_usermanager_user(user_id: str) -> Optional[Users]: + row = await db.fetchone("SELECT * FROM usermanager.users WHERE id = ?", (user_id,)) + return Users(**row) if row else None + + +async def get_usermanager_users(user_id: str) -> List[Users]: + rows = await db.fetchall( + "SELECT * FROM usermanager.users WHERE admin = ?", (user_id,) + ) + return [Users(**row) for row in rows] + + +async def delete_usermanager_user(user_id: str) -> None: + wallets = await get_usermanager_wallets(user_id) + for wallet in wallets: + await delete_wallet(user_id=user_id, wallet_id=wallet.id) + + await db.execute("DELETE FROM usermanager.users WHERE id = ?", (user_id,)) + await db.execute("""DELETE FROM usermanager.wallets WHERE "user" = ?""", (user_id,)) + + +### Wallets + + +async def create_usermanager_wallet( + user_id: str, wallet_name: str, admin_id: str +) -> Wallets: + wallet = await create_wallet(user_id=user_id, wallet_name=wallet_name) + await db.execute( + """ + INSERT INTO usermanager.wallets (id, admin, name, "user", adminkey, inkey) + VALUES (?, ?, ?, ?, ?, ?) + """, + (wallet.id, admin_id, wallet_name, user_id, wallet.adminkey, wallet.inkey), + ) + wallet_created = await get_usermanager_wallet(wallet.id) + assert wallet_created, "Newly created wallet couldn't be retrieved" + return wallet_created + + +async def get_usermanager_wallet(wallet_id: str) -> Optional[Wallets]: + row = await db.fetchone( + "SELECT * FROM usermanager.wallets WHERE id = ?", (wallet_id,) + ) + return Wallets(**row) if row else None + + +async def get_usermanager_wallets(admin_id: str) -> Optional[Wallets]: + rows = await db.fetchall( + "SELECT * FROM usermanager.wallets WHERE admin = ?", (admin_id,) + ) + return [Wallets(**row) for row in rows] + + +async def get_usermanager_users_wallets(user_id: str) -> Optional[Wallets]: + rows = await db.fetchall( + """SELECT * FROM usermanager.wallets WHERE "user" = ?""", (user_id,) + ) + return [Wallets(**row) for row in rows] + + +async def get_usermanager_wallet_transactions(wallet_id: str) -> Optional[Payment]: + return await get_payments( + wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True + ) + + +async def delete_usermanager_wallet(wallet_id: str, user_id: str) -> None: + await delete_wallet(user_id=user_id, wallet_id=wallet_id) + await db.execute("DELETE FROM usermanager.wallets WHERE id = ?", (wallet_id,)) diff --git a/lnbits/extensions/usermanager/migrations.py b/lnbits/extensions/usermanager/migrations.py new file mode 100644 index 00000000..62a21575 --- /dev/null +++ b/lnbits/extensions/usermanager/migrations.py @@ -0,0 +1,31 @@ +async def m001_initial(db): + """ + Initial users table. + """ + await db.execute( + """ + CREATE TABLE usermanager.users ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + admin TEXT NOT NULL, + email TEXT, + password TEXT + ); + """ + ) + + """ + Initial wallets table. + """ + await db.execute( + """ + CREATE TABLE usermanager.wallets ( + id TEXT PRIMARY KEY, + admin TEXT NOT NULL, + name TEXT NOT NULL, + "user" TEXT NOT NULL, + adminkey TEXT NOT NULL, + inkey TEXT NOT NULL + ); + """ + ) diff --git a/lnbits/extensions/usermanager/models.py b/lnbits/extensions/usermanager/models.py new file mode 100644 index 00000000..97eaaea8 --- /dev/null +++ b/lnbits/extensions/usermanager/models.py @@ -0,0 +1,23 @@ +from typing import NamedTuple +from sqlite3 import Row + + +class Users(NamedTuple): + id: str + name: str + admin: str + email: str + password: str + + +class Wallets(NamedTuple): + id: str + admin: str + name: str + user: str + adminkey: str + inkey: str + + @classmethod + def from_row(cls, row: Row) -> "Wallets": + return cls(**dict(row)) diff --git a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html new file mode 100644 index 00000000..74640bb8 --- /dev/null +++ b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html @@ -0,0 +1,259 @@ + + + +
+ User Manager: Make and manager users/wallets +
+

+ To help developers use LNbits to manage their users, the User Manager + extension allows the creation and management of users and wallets. +
For example, a games developer may be developing a game that needs + each user to have their own wallet, LNbits can be included in the + develpoers stack as the user and wallet manager.
+ + Created by, Ben Arc +

+
+
+
+ + + + + GET + /usermanager/api/v1/users +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON list of users +
Curl example
+ curl -X GET {{ request.url_root }}usermanager/api/v1/users -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /usermanager/api/v1/users/<user_id> +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON list of users +
Curl example
+ curl -X GET {{ request.url_root }}usermanager/api/v1/users/<user_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /usermanager/api/v1/wallets/<user_id> +
Headers
+ {"X-Api-Key": <string>} +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON wallet data +
Curl example
+ curl -X GET {{ request.url_root }}usermanager/api/v1/wallets/<user_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /usermanager/api/v1/wallets<wallet_id> +
Headers
+ {"X-Api-Key": <string>} +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON a wallets transactions +
Curl example
+ curl -X GET {{ request.url_root }}usermanager/api/v1/wallets<wallet_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + POST + /usermanager/api/v1/users +
Headers
+ {"X-Api-Key": <string>, "Content-type": + "application/json"} +
+ Body (application/json) - "admin_id" is a YOUR user ID +
+ {"admin_id": <string>, "user_name": <string>, + "wallet_name": <string>,"email": <Optional string> + ,"password": <Optional string>} +
+ Returns 201 CREATED (application/json) +
+ {"id": <string>, "name": <string>, "admin": + <string>, "email": <string>, "password": + <string>} +
Curl example
+ curl -X POST {{ request.url_root }}usermanager/api/v1/users -d '{"admin_id": "{{ + g.user.id }}", "wallet_name": <string>, "user_name": + <string>, "email": <Optional string>, "password": < + Optional string>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H + "Content-type: application/json" + +
+
+
+ + + + POST + /usermanager/api/v1/wallets +
Headers
+ {"X-Api-Key": <string>, "Content-type": + "application/json"} +
+ Body (application/json) - "admin_id" is a YOUR user ID +
+ {"user_id": <string>, "wallet_name": <string>, + "admin_id": <string>} +
+ Returns 201 CREATED (application/json) +
+ {"id": <string>, "admin": <string>, "name": + <string>, "user": <string>, "adminkey": <string>, + "inkey": <string>} +
Curl example
+ curl -X POST {{ request.url_root }}usermanager/api/v1/wallets -d '{"user_id": + <string>, "wallet_name": <string>, "admin_id": "{{ + g.user.id }}"}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H + "Content-type: application/json" + +
+
+
+ + + + DELETE + /usermanager/api/v1/users/<user_id> +
Headers
+ {"X-Api-Key": <string>} +
Curl example
+ curl -X DELETE {{ request.url_root }}usermanager/api/v1/users/<user_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + DELETE + /usermanager/api/v1/wallets/<wallet_id> +
Headers
+ {"X-Api-Key": <string>} +
Curl example
+ curl -X DELETE {{ request.url_root }}usermanager/api/v1/wallets/<wallet_id> + -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + POST + /usermanager/api/v1/extensions +
Headers
+ {"X-Api-Key": <string>} +
Curl example
+ curl -X POST {{ request.url_root }}usermanager/api/v1/extensions -d '{"userid": + <string>, "extension": <string>, "active": + <integer>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H + "Content-type: application/json" + +
+
+
+
diff --git a/lnbits/extensions/usermanager/templates/usermanager/index.html b/lnbits/extensions/usermanager/templates/usermanager/index.html new file mode 100644 index 00000000..446ee51d --- /dev/null +++ b/lnbits/extensions/usermanager/templates/usermanager/index.html @@ -0,0 +1,473 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + New User + New Wallet + + + + + + +
+
+
Users
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + +
+
+
Wallets
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+ +
+ + +
+ {{SITE_TITLE}} User Manager Extension +
+
+ + + {% include "usermanager/_api_docs.html" %} + +
+
+ + + + + + + + + + Create User + Cancel + + + + + + + + + + + Create Wallet + Cancel + + + +
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/usermanager/views.py b/lnbits/extensions/usermanager/views.py new file mode 100644 index 00000000..df6949c6 --- /dev/null +++ b/lnbits/extensions/usermanager/views.py @@ -0,0 +1,12 @@ +from quart import g, render_template + +from lnbits.decorators import check_user_exists, validate_uuids + +from . import usermanager_ext + + +@usermanager_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("usermanager/index.html", user=g.user) diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py new file mode 100644 index 00000000..d3bba6ad --- /dev/null +++ b/lnbits/extensions/usermanager/views_api.py @@ -0,0 +1,156 @@ +from quart import g, jsonify +from http import HTTPStatus + +from lnbits.core.crud import get_user +from lnbits.decorators import api_check_wallet_key, api_validate_post_request + +from . import usermanager_ext +from .crud import ( + create_usermanager_user, + get_usermanager_user, + get_usermanager_users, + get_usermanager_wallet_transactions, + delete_usermanager_user, + create_usermanager_wallet, + get_usermanager_wallet, + get_usermanager_wallets, + get_usermanager_users_wallets, + delete_usermanager_wallet, +) +from lnbits.core import update_user_extension + + +### Users + + +@usermanager_ext.route("/api/v1/users", methods=["GET"]) +@api_check_wallet_key(key_type="invoice") +async def api_usermanager_users(): + user_id = g.wallet.user + return ( + jsonify([user._asdict() for user in await get_usermanager_users(user_id)]), + HTTPStatus.OK, + ) + + +@usermanager_ext.route("/api/v1/users/", methods=["GET"]) +@api_check_wallet_key(key_type="invoice") +async def api_usermanager_user(user_id): + user = await get_usermanager_user(user_id) + return ( + jsonify(user._asdict()), + HTTPStatus.OK, + ) + + +@usermanager_ext.route("/api/v1/users", methods=["POST"]) +@api_check_wallet_key(key_type="invoice") +@api_validate_post_request( + schema={ + "user_name": {"type": "string", "empty": False, "required": True}, + "wallet_name": {"type": "string", "empty": False, "required": True}, + "admin_id": {"type": "string", "empty": False, "required": True}, + "email": {"type": "string", "required": False}, + "password": {"type": "string", "required": False}, + } +) +async def api_usermanager_users_create(): + user = await create_usermanager_user(**g.data) + full = user._asdict() + full["wallets"] = [wallet._asdict() for wallet in await get_usermanager_users_wallets(user.id)] + return jsonify(full), HTTPStatus.CREATED + + +@usermanager_ext.route("/api/v1/users/", methods=["DELETE"]) +@api_check_wallet_key(key_type="invoice") +async def api_usermanager_users_delete(user_id): + user = await get_usermanager_user(user_id) + if not user: + return jsonify({"message": "User does not exist."}), HTTPStatus.NOT_FOUND + await delete_usermanager_user(user_id) + return "", HTTPStatus.NO_CONTENT + + +###Activate Extension + + +@usermanager_ext.route("/api/v1/extensions", methods=["POST"]) +@api_check_wallet_key(key_type="invoice") +@api_validate_post_request( + schema={ + "extension": {"type": "string", "empty": False, "required": True}, + "userid": {"type": "string", "empty": False, "required": True}, + "active": {"type": "boolean", "required": True}, + } +) +async def api_usermanager_activate_extension(): + user = await get_user(g.data["userid"]) + if not user: + return jsonify({"message": "no such user"}), HTTPStatus.NOT_FOUND + update_user_extension( + user_id=g.data["userid"], extension=g.data["extension"], active=g.data["active"] + ) + return jsonify({"extension": "updated"}), HTTPStatus.CREATED + + +###Wallets + + +@usermanager_ext.route("/api/v1/wallets", methods=["POST"]) +@api_check_wallet_key(key_type="invoice") +@api_validate_post_request( + schema={ + "user_id": {"type": "string", "empty": False, "required": True}, + "wallet_name": {"type": "string", "empty": False, "required": True}, + "admin_id": {"type": "string", "empty": False, "required": True}, + } +) +async def api_usermanager_wallets_create(): + user = await create_usermanager_wallet( + g.data["user_id"], g.data["wallet_name"], g.data["admin_id"] + ) + return jsonify(user._asdict()), HTTPStatus.CREATED + + +@usermanager_ext.route("/api/v1/wallets", methods=["GET"]) +@api_check_wallet_key(key_type="invoice") +async def api_usermanager_wallets(): + admin_id = g.wallet.user + return ( + jsonify( + [wallet._asdict() for wallet in await get_usermanager_wallets(admin_id)] + ), + HTTPStatus.OK, + ) + + +@usermanager_ext.route("/api/v1/wallets", methods=["GET"]) +@api_check_wallet_key(key_type="invoice") +async def api_usermanager_wallet_transactions(wallet_id): + return jsonify(await get_usermanager_wallet_transactions(wallet_id)), HTTPStatus.OK + + +@usermanager_ext.route("/api/v1/wallets/", methods=["GET"]) +@api_check_wallet_key(key_type="invoice") +async def api_usermanager_users_wallets(user_id): + wallet = await get_usermanager_users_wallets(user_id) + return ( + jsonify( + [ + wallet._asdict() + for wallet in await get_usermanager_users_wallets(user_id) + ] + ), + HTTPStatus.OK, + ) + + +@usermanager_ext.route("/api/v1/wallets/", methods=["DELETE"]) +@api_check_wallet_key(key_type="invoice") +async def api_usermanager_wallets_delete(wallet_id): + wallet = await get_usermanager_wallet(wallet_id) + if not wallet: + return jsonify({"message": "Wallet does not exist."}), HTTPStatus.NOT_FOUND + + await delete_usermanager_wallet(wallet_id, wallet.user) + return "", HTTPStatus.NO_CONTENT From ca3b1d74551707af11ded4e7cfa0352d58637ac3 Mon Sep 17 00:00:00 2001 From: benarc Date: Tue, 12 Oct 2021 09:01:54 +0100 Subject: [PATCH 06/75] fires up ok --- lnbits/extensions/jukebox/models.py | 1 + .../jukebox/templates/jukebox/_api_docs.html | 8 ++++---- lnbits/extensions/jukebox/views.py | 4 +++- lnbits/extensions/jukebox/views_api.py | 14 +++++++------- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py index d50c830b..8752ae85 100644 --- a/lnbits/extensions/jukebox/models.py +++ b/lnbits/extensions/jukebox/models.py @@ -1,6 +1,7 @@ from typing import NamedTuple from sqlite3 import Row from fastapi.param_functions import Query +from pydantic.main import BaseModel class CreateJukeLinkData(BaseModel): diff --git a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html index f5a91313..b1968b48 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html +++ b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html @@ -38,7 +38,7 @@
Curl example
curl -X GET {{ request.url_root }}api/v1/jukebox -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" + user.wallets[0].adminkey }}" @@ -60,7 +60,7 @@
Curl example
curl -X GET {{ request.url_root }}api/v1/jukebox/<juke_id> -H - "X-Api-Key: {{ g.user.wallets[0].adminkey }}" + "X-Api-Key: {{ user.wallets[0].adminkey }}" @@ -95,7 +95,7 @@ "sp_device": <string, spotify_user_secret>, "sp_playlists": <string, not_required>, "price": <integer, not_required>}' -H "Content-type: application/json" -H "X-Api-Key: - {{g.user.wallets[0].adminkey }}" + {{user.wallets[0].adminkey }}" @@ -117,7 +117,7 @@
Curl example
curl -X DELETE {{ request.url_root }}api/v1/jukebox/<juke_id> - -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}" + -H "X-Api-Key: {{ user.wallets[0].adminkey }}" diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py index 360f75e3..d8a7c3f6 100644 --- a/lnbits/extensions/jukebox/views.py +++ b/lnbits/extensions/jukebox/views.py @@ -13,6 +13,7 @@ from starlette.responses import HTMLResponse from lnbits.core.models import User, Payment from .views_api import api_get_jukebox_device_check + templates = Jinja2Templates(directory="templates") @@ -49,5 +50,6 @@ async def connect_to_jukebox(request: Request, juke_id): ) else: return jukebox_renderer().TemplateResponse( - "jukebox/error.html", {"request": request} + "jukebox/error.html", + {"request": request, "jukebox": jukebox.jukebox(req=request)}, ) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 44575c6b..799628e3 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -9,13 +9,11 @@ import json from typing import Optional from fastapi.params import Depends from fastapi.param_functions import Query -from pydantic.main import BaseModel from .models import CreateJukeLinkData from lnbits.decorators import ( check_user_exists, WalletTypeInfo, get_key_type, - api_check_wallet_key, api_validate_post_request, ) import httpx @@ -89,17 +87,19 @@ async def api_check_credentials_callbac( @jukebox_ext.get("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK) async def api_check_credentials_check( - juke_id=None, wallet: WalletTypeInfo = Depends(get_key_type) + juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) ): + print(juke_id) jukebox = await get_jukebox(juke_id) - return jukebox._asdict() + + return jukebox @jukebox_ext.post("/api/v1/jukebox", status_code=HTTPStatus.CREATED) @jukebox_ext.put("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK) async def api_create_update_jukebox( data: CreateJukeLinkData, - juke_id=None, + juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type), ): if juke_id: @@ -107,7 +107,7 @@ async def api_create_update_jukebox( else: jukebox = await create_jukebox(inkey=g.wallet.inkey, **g.data) - return jukebox._asdict() + return jukebox.dict() @jukebox_ext.delete("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK) @@ -117,7 +117,7 @@ async def api_delete_item( ): await delete_jukebox(juke_id) try: - return [{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)] + return [{**jukebox.dict()} for jukebox in await get_jukeboxs(g.wallet.user)] except: raise HTTPException( status_code=HTTPStatus.NO_CONTENT, From f94543943fd17e664b79e99ccbc794e22eba68a1 Mon Sep 17 00:00:00 2001 From: benarc Date: Tue, 12 Oct 2021 09:44:41 +0100 Subject: [PATCH 07/75] latest --- lnbits/extensions/jukebox/views_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 799628e3..571676b9 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -36,7 +36,7 @@ async def api_get_jukeboxs( wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False), ): - wallet_user = wallet.wallet[0].user + wallet_user = wallet.wallet.user try: return [ From a33b24d6dc7459cf81ca233751cab454315c7055 Mon Sep 17 00:00:00 2001 From: benarc Date: Tue, 12 Oct 2021 09:59:01 +0100 Subject: [PATCH 08/75] Liberated HTTPStatus.OKs --- lnbits/extensions/jukebox/views_api.py | 29 +++++++++----------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 571676b9..79a9a623 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -30,7 +30,7 @@ from .crud import ( ) -@jukebox_ext.get("/api/v1/jukebox", status_code=HTTPStatus.OK) +@jukebox_ext.get("/api/v1/jukebox") async def api_get_jukeboxs( req: Request, wallet: WalletTypeInfo = Depends(get_key_type), @@ -54,7 +54,7 @@ async def api_get_jukeboxs( ##################SPOTIFY AUTH##################### -@jukebox_ext.get("/api/v1/jukebox/spotify/cb/", status_code=HTTPStatus.OK) +@jukebox_ext.get("/api/v1/jukebox/spotify/cb/") async def api_check_credentials_callbac( juke_id: str = Query(None), code: str = Query(None), @@ -85,7 +85,7 @@ async def api_check_credentials_callbac( return "

Success!

You can close this window

" -@jukebox_ext.get("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK) +@jukebox_ext.get("/api/v1/jukebox/{juke_id}") async def api_check_credentials_check( juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) ): @@ -110,7 +110,7 @@ async def api_create_update_jukebox( return jukebox.dict() -@jukebox_ext.delete("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK) +@jukebox_ext.delete("/api/v1/jukebox/{juke_id}") async def api_delete_item( juke_id=None, wallet: WalletTypeInfo = Depends(get_key_type), @@ -130,9 +130,7 @@ async def api_delete_item( ######GET ACCESS TOKEN###### -@jukebox_ext.get( - "/api/v1/jukebox/jb/playlist/{juke_id}/{sp_playlist}", status_code=HTTPStatus.OK -) +@jukebox_ext.get("/api/v1/jukebox/jb/playlist/{juke_id}/{sp_playlist}") async def api_get_jukebox_song( juke_id: str = Query(None), sp_playlist: str = Query(None), @@ -225,7 +223,7 @@ async def api_get_token(juke_id=None): ######CHECK DEVICE -@jukebox_ext.get("/api/v1/jukebox/jb/{juke_id}", status_code=HTTPStatus.OK) +@jukebox_ext.get("/api/v1/jukebox/jb/{juke_id}") async def api_get_jukebox_device_check( juke_id: str = Query(None), retry: bool = Query(False), @@ -270,9 +268,7 @@ async def api_get_jukebox_device_check( ######GET INVOICE STUFF -@jukebox_ext.get( - "/api/v1/jukebox/jb/invoice/{juke_id}/{song_id}", status_code=HTTPStatus.OK -) +@jukebox_ext.get("/api/v1/jukebox/jb/invoice/{juke_id}/{song_id}") async def api_get_jukebox_invoice( juke_id: str = Query(None), song_id: str = Query(None), @@ -314,9 +310,7 @@ async def api_get_jukebox_invoice( return {invoice, jukebox_payment} -@jukebox_ext.get( - "/api/v1/jukebox/jb/checkinvoice/{pay_hash}/{juke_id}", status_code=HTTPStatus.OK -) +@jukebox_ext.get("/api/v1/jukebox/jb/checkinvoice/{pay_hash}/{juke_id}") async def api_get_jukebox_invoice_check( pay_hash: str = Query(None), juke_id: str = Query(None), @@ -342,10 +336,7 @@ async def api_get_jukebox_invoice_check( return {"paid": False} -@jukebox_ext.get( - "/api/v1/jukebox/jb/invoicep/{song_id}/{juke_id}/{pay_hash}", - status_code=HTTPStatus.OK, -) +@jukebox_ext.get("/api/v1/jukebox/jb/invoicep/{song_id}/{juke_id}/{pay_hash}") async def api_get_jukebox_invoice_paid( song_id: str = Query(None), juke_id: str = Query(None), @@ -469,7 +460,7 @@ async def api_get_jukebox_invoice_paid( ############################GET TRACKS -@jukebox_ext.get("/api/v1/jukebox/jb/currently/{juke_id}", status_code=HTTPStatus.OK) +@jukebox_ext.get("/api/v1/jukebox/jb/currently/{juke_id}") async def api_get_jukebox_currently( retry: bool = Query(False), juke_id: str = Query(None), From e5b22ead0c5ec2debcd1612d3aad9a1c608015d2 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Tue, 12 Oct 2021 10:38:09 +0100 Subject: [PATCH 09/75] initial work --- lnbits/extensions/usermanager/__init__.py | 19 ++++- lnbits/extensions/usermanager/models.py | 14 +++- lnbits/extensions/usermanager/views.py | 19 +++-- lnbits/extensions/usermanager/views_api.py | 95 ++++++++++------------ 4 files changed, 81 insertions(+), 66 deletions(-) diff --git a/lnbits/extensions/usermanager/__init__.py b/lnbits/extensions/usermanager/__init__.py index 53154812..f421a15c 100644 --- a/lnbits/extensions/usermanager/__init__.py +++ b/lnbits/extensions/usermanager/__init__.py @@ -1,12 +1,25 @@ -from quart import Blueprint +import asyncio + +from fastapi import APIRouter + from lnbits.db import Database +from lnbits.helpers import template_renderer db = Database("ext_usermanager") -usermanager_ext: Blueprint = Blueprint( - "usermanager", __name__, static_folder="static", template_folder="templates" +usermanager_ext: APIRouter = APIRouter( + prefix="/usermanager", + tags=["usermanager"] + #"usermanager", __name__, static_folder="static", template_folder="templates" ) +def usermanager_renderer(): + return template_renderer( + [ + "lnbits/extensions/usermanager/templates", + ] + ) + from .views_api import * # noqa from .views import * # noqa diff --git a/lnbits/extensions/usermanager/models.py b/lnbits/extensions/usermanager/models.py index 97eaaea8..7e6a9595 100644 --- a/lnbits/extensions/usermanager/models.py +++ b/lnbits/extensions/usermanager/models.py @@ -1,8 +1,16 @@ -from typing import NamedTuple +from pydantic import BaseModel +from fastapi.param_functions import Query from sqlite3 import Row +class CreateUserData(BaseModel): + user_name: str = Query(...) + wallet_name: str = Query(...) + admin_id: str = Query(...) + email: str = Query(None) + password: str = Query(None) -class Users(NamedTuple): + +class Users(BaseModel): id: str name: str admin: str @@ -10,7 +18,7 @@ class Users(NamedTuple): password: str -class Wallets(NamedTuple): +class Wallets(BaseModel): id: str admin: str name: str diff --git a/lnbits/extensions/usermanager/views.py b/lnbits/extensions/usermanager/views.py index df6949c6..d58a9826 100644 --- a/lnbits/extensions/usermanager/views.py +++ b/lnbits/extensions/usermanager/views.py @@ -1,12 +1,13 @@ -from quart import g, render_template +from fastapi import FastAPI, Request +from fastapi.params import Depends +from fastapi.templating import Jinja2Templates +from starlette.responses import HTMLResponse -from lnbits.decorators import check_user_exists, validate_uuids +from lnbits.core.models import User +from lnbits.decorators import check_user_exists -from . import usermanager_ext +from . import usermanager_ext, usermanager_renderer - -@usermanager_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(): - return await render_template("usermanager/index.html", user=g.user) +@usermanager_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return await render_template("usermanager/index.html", user=user.dict()) diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py index d3bba6ad..4e41c8aa 100644 --- a/lnbits/extensions/usermanager/views_api.py +++ b/lnbits/extensions/usermanager/views_api.py @@ -1,10 +1,15 @@ -from quart import g, jsonify from http import HTTPStatus +from starlette.exceptions import HTTPException + +from fastapi import Query +from fastapi.params import Depends from lnbits.core.crud import get_user from lnbits.decorators import api_check_wallet_key, api_validate_post_request +from lnbits.decorators import WalletTypeInfo, get_key_type from . import usermanager_ext +from .models import CreateUserData from .crud import ( create_usermanager_user, get_usermanager_user, @@ -23,74 +28,62 @@ from lnbits.core import update_user_extension ### Users -@usermanager_ext.route("/api/v1/users", methods=["GET"]) -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_users(): - user_id = g.wallet.user - return ( - jsonify([user._asdict() for user in await get_usermanager_users(user_id)]), - HTTPStatus.OK, - ) +@usermanager_ext.get("/api/v1/users", status_code=HTTPStatus.OK) +async def api_usermanager_users(wallet: WalletTypeInfo = Depends(get_key_type)): + user_id = wallet.wallet.user + return [user.dict() for user in await get_usermanager_users(user_id)] -@usermanager_ext.route("/api/v1/users/", methods=["GET"]) -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_user(user_id): +@usermanager_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK) +async def api_usermanager_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)): user = await get_usermanager_user(user_id) - return ( - jsonify(user._asdict()), - HTTPStatus.OK, - ) + return user.dict() -@usermanager_ext.route("/api/v1/users", methods=["POST"]) -@api_check_wallet_key(key_type="invoice") -@api_validate_post_request( - schema={ - "user_name": {"type": "string", "empty": False, "required": True}, - "wallet_name": {"type": "string", "empty": False, "required": True}, - "admin_id": {"type": "string", "empty": False, "required": True}, - "email": {"type": "string", "required": False}, - "password": {"type": "string", "required": False}, - } -) -async def api_usermanager_users_create(): - user = await create_usermanager_user(**g.data) - full = user._asdict() - full["wallets"] = [wallet._asdict() for wallet in await get_usermanager_users_wallets(user.id)] - return jsonify(full), HTTPStatus.CREATED +@usermanager_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED) +# @api_validate_post_request( +# schema={ +# "user_name": {"type": "string", "empty": False, "required": True}, +# "wallet_name": {"type": "string", "empty": False, "required": True}, +# "admin_id": {"type": "string", "empty": False, "required": True}, +# "email": {"type": "string", "required": False}, +# "password": {"type": "string", "required": False}, +# } +# ) +async def api_usermanager_users_create(data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type)): + user = await create_usermanager_user(**data) + full = user.dict() + full["wallets"] = [wallet.dict() for wallet in await get_usermanager_users_wallets(user.id)] + return full -@usermanager_ext.route("/api/v1/users/", methods=["DELETE"]) -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_users_delete(user_id): +@usermanager_ext.delete("/api/v1/users/{user_id}") +async def api_usermanager_users_delete(user_id, wallet: WalletTypeInfo = Depends(get_key_type)): user = await get_usermanager_user(user_id) if not user: - return jsonify({"message": "User does not exist."}), HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="User does not exist." + ) await delete_usermanager_user(user_id) - return "", HTTPStatus.NO_CONTENT + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) ###Activate Extension -@usermanager_ext.route("/api/v1/extensions", methods=["POST"]) -@api_check_wallet_key(key_type="invoice") -@api_validate_post_request( - schema={ - "extension": {"type": "string", "empty": False, "required": True}, - "userid": {"type": "string", "empty": False, "required": True}, - "active": {"type": "boolean", "required": True}, - } -) -async def api_usermanager_activate_extension(): - user = await get_user(g.data["userid"]) +@usermanager_ext.post("/api/v1/extensions") +async def api_usermanager_activate_extension(extension: str = Query(...), userid: str = Query(...), active: bool = Query(...)): + user = await get_user(userid) if not user: - return jsonify({"message": "no such user"}), HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="User does not exist." + ) update_user_extension( - user_id=g.data["userid"], extension=g.data["extension"], active=g.data["active"] + user_id=userid, extension=extension, active=active ) - return jsonify({"extension": "updated"}), HTTPStatus.CREATED + return {"extension": "updated"} ###Wallets From f4dfc1b68978f1e52e2dbdc234850febcd337db7 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Tue, 12 Oct 2021 10:57:15 +0100 Subject: [PATCH 10/75] usermanager views_api --- lnbits/extensions/usermanager/views_api.py | 79 ++++++++-------------- 1 file changed, 30 insertions(+), 49 deletions(-) diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py index 4e41c8aa..e8a78bf1 100644 --- a/lnbits/extensions/usermanager/views_api.py +++ b/lnbits/extensions/usermanager/views_api.py @@ -5,7 +5,6 @@ from fastapi import Query from fastapi.params import Depends from lnbits.core.crud import get_user -from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import WalletTypeInfo, get_key_type from . import usermanager_ext @@ -89,61 +88,43 @@ async def api_usermanager_activate_extension(extension: str = Query(...), userid ###Wallets -@usermanager_ext.route("/api/v1/wallets", methods=["POST"]) -@api_check_wallet_key(key_type="invoice") -@api_validate_post_request( - schema={ - "user_id": {"type": "string", "empty": False, "required": True}, - "wallet_name": {"type": "string", "empty": False, "required": True}, - "admin_id": {"type": "string", "empty": False, "required": True}, - } -) -async def api_usermanager_wallets_create(): +@usermanager_ext.post("/api/v1/wallets") +async def api_usermanager_wallets_create( + wallet: WalletTypeInfo = Depends(get_key_type), + user_id: str = Query(...), + wallet_name: str = Query(...), + admin_id: str = Query(...) +): user = await create_usermanager_wallet( - g.data["user_id"], g.data["wallet_name"], g.data["admin_id"] + user_id, wallet_name, admin_id ) - return jsonify(user._asdict()), HTTPStatus.CREATED + return user.dict() -@usermanager_ext.route("/api/v1/wallets", methods=["GET"]) -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_wallets(): - admin_id = g.wallet.user - return ( - jsonify( - [wallet._asdict() for wallet in await get_usermanager_wallets(admin_id)] - ), - HTTPStatus.OK, - ) +@usermanager_ext.get("/api/v1/wallets") +async def api_usermanager_wallets(wallet: WalletTypeInfo = Depends(get_key_type)): + admin_id = wallet.wallet.user + return [wallet.dict() for wallet in await get_usermanager_wallets(admin_id)] -@usermanager_ext.route("/api/v1/wallets", methods=["GET"]) -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_wallet_transactions(wallet_id): - return jsonify(await get_usermanager_wallet_transactions(wallet_id)), HTTPStatus.OK +@usermanager_ext.get("/api/v1/wallets/{wallet_id}") +async def api_usermanager_wallet_transactions(wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)): + return await get_usermanager_wallet_transactions(wallet_id) -@usermanager_ext.route("/api/v1/wallets/", methods=["GET"]) -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_users_wallets(user_id): - wallet = await get_usermanager_users_wallets(user_id) - return ( - jsonify( - [ - wallet._asdict() - for wallet in await get_usermanager_users_wallets(user_id) - ] - ), - HTTPStatus.OK, - ) +@usermanager_ext.get("/api/v1/wallets/{user_id}") +async def api_usermanager_users_wallets(user_id, wallet: WalletTypeInfo = Depends(get_key_type)): + # wallet = await get_usermanager_users_wallets(user_id) + return [s_wallet.dict() for s_wallet in await get_usermanager_users_wallets(user_id)] -@usermanager_ext.route("/api/v1/wallets/", methods=["DELETE"]) -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_wallets_delete(wallet_id): - wallet = await get_usermanager_wallet(wallet_id) - if not wallet: - return jsonify({"message": "Wallet does not exist."}), HTTPStatus.NOT_FOUND - - await delete_usermanager_wallet(wallet_id, wallet.user) - return "", HTTPStatus.NO_CONTENT +@usermanager_ext.delete("/api/v1/wallets/{wallet_id}") +async def api_usermanager_wallets_delete(wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)): + get_wallet = await get_usermanager_wallet(wallet_id) + if not get_wallet: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Wallet does not exist." + ) + await delete_usermanager_wallet(wallet_id, get_wallet.user) + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) From 04462f337ffa2fc0f43b103e0ef848b926102c6a Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Tue, 12 Oct 2021 17:04:49 +0100 Subject: [PATCH 11/75] usermanager working --- lnbits/extensions/usermanager/crud.py | 15 +++--- lnbits/extensions/usermanager/models.py | 4 +- .../templates/usermanager/_api_docs.html | 53 ++++++++++--------- .../templates/usermanager/index.html | 1 + lnbits/extensions/usermanager/views.py | 2 +- lnbits/extensions/usermanager/views_api.py | 2 +- 6 files changed, 40 insertions(+), 37 deletions(-) diff --git a/lnbits/extensions/usermanager/crud.py b/lnbits/extensions/usermanager/crud.py index a7854ad8..e1c8aebd 100644 --- a/lnbits/extensions/usermanager/crud.py +++ b/lnbits/extensions/usermanager/crud.py @@ -10,31 +10,27 @@ from lnbits.core.crud import ( ) from . import db -from .models import Users, Wallets +from .models import Users, Wallets, CreateUserData ### Users async def create_usermanager_user( - user_name: str, - wallet_name: str, - admin_id: str, - email: Optional[str] = None, - password: Optional[str] = None, + data: CreateUserData ) -> Users: account = await create_account() user = await get_user(account.id) assert user, "Newly created user couldn't be retrieved" - wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name) + wallet = await create_wallet(user_id=user.id, wallet_name=data.wallet_name) await db.execute( """ INSERT INTO usermanager.users (id, name, admin, email, password) VALUES (?, ?, ?, ?, ?) """, - (user.id, user_name, admin_id, email, password), + (user.id, data.user_name, data.admin_id, data.email, data.password), ) await db.execute( @@ -42,7 +38,7 @@ async def create_usermanager_user( INSERT INTO usermanager.wallets (id, admin, name, "user", adminkey, inkey) VALUES (?, ?, ?, ?, ?, ?) """, - (wallet.id, admin_id, wallet_name, user.id, wallet.adminkey, wallet.inkey), + (wallet.id, data.admin_id, data.wallet_name, user.id, wallet.adminkey, wallet.inkey), ) user_created = await get_usermanager_user(user.id) @@ -59,6 +55,7 @@ async def get_usermanager_users(user_id: str) -> List[Users]: rows = await db.fetchall( "SELECT * FROM usermanager.users WHERE admin = ?", (user_id,) ) + return [Users(**row) for row in rows] diff --git a/lnbits/extensions/usermanager/models.py b/lnbits/extensions/usermanager/models.py index 7e6a9595..005ed8af 100644 --- a/lnbits/extensions/usermanager/models.py +++ b/lnbits/extensions/usermanager/models.py @@ -6,8 +6,8 @@ class CreateUserData(BaseModel): user_name: str = Query(...) wallet_name: str = Query(...) admin_id: str = Query(...) - email: str = Query(None) - password: str = Query(None) + email: str = Query("") + password: str = Query("") class Users(BaseModel): diff --git a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html index 74640bb8..1944416b 100644 --- a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html +++ b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html @@ -42,8 +42,8 @@ JSON list of users
Curl example
curl -X GET {{ request.url_root }}usermanager/api/v1/users -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root }}usermanager/api/v1/users -H + "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -62,8 +62,9 @@ JSON list of users
Curl example
curl -X GET {{ request.url_root }}usermanager/api/v1/users/<user_id> -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root + }}usermanager/api/v1/users/<user_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -84,8 +85,9 @@ JSON wallet data
Curl example
curl -X GET {{ request.url_root }}usermanager/api/v1/wallets/<user_id> -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root + }}usermanager/api/v1/wallets/<user_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -106,8 +108,9 @@ JSON a wallets transactions
Curl example
curl -X GET {{ request.url_root }}usermanager/api/v1/wallets<wallet_id> -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root + }}usermanager/api/v1/wallets<wallet_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -147,11 +150,11 @@ >
Curl example
curl -X POST {{ request.url_root }}usermanager/api/v1/users -d '{"admin_id": "{{ - g.user.id }}", "wallet_name": <string>, "user_name": - <string>, "email": <Optional string>, "password": < - Optional string>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H - "Content-type: application/json" + >curl -X POST {{ request.url_root }}usermanager/api/v1/users -d + '{"admin_id": "{{ user.id }}", "wallet_name": <string>, + "user_name": <string>, "email": <Optional string>, + "password": < Optional string>}' -H "X-Api-Key: {{ + user.wallets[0].inkey }}" -H "Content-type: application/json" @@ -185,10 +188,10 @@ >
Curl example
curl -X POST {{ request.url_root }}usermanager/api/v1/wallets -d '{"user_id": - <string>, "wallet_name": <string>, "admin_id": "{{ - g.user.id }}"}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H - "Content-type: application/json" + >curl -X POST {{ request.url_root }}usermanager/api/v1/wallets -d + '{"user_id": <string>, "wallet_name": <string>, + "admin_id": "{{ user.id }}"}' -H "X-Api-Key: {{ user.wallets[0].inkey + }}" -H "Content-type: application/json" @@ -209,8 +212,9 @@ {"X-Api-Key": <string>}
Curl example
curl -X DELETE {{ request.url_root }}usermanager/api/v1/users/<user_id> -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" + >curl -X DELETE {{ request.url_root + }}usermanager/api/v1/users/<user_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -226,8 +230,9 @@ {"X-Api-Key": <string>}
Curl example
curl -X DELETE {{ request.url_root }}usermanager/api/v1/wallets/<wallet_id> - -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" + >curl -X DELETE {{ request.url_root + }}usermanager/api/v1/wallets/<wallet_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" @@ -248,9 +253,9 @@ {"X-Api-Key": <string>}
Curl example
curl -X POST {{ request.url_root }}usermanager/api/v1/extensions -d '{"userid": - <string>, "extension": <string>, "active": - <integer>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H + >curl -X POST {{ request.url_root }}usermanager/api/v1/extensions -d + '{"userid": <string>, "extension": <string>, "active": + <integer>}' -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type: application/json" diff --git a/lnbits/extensions/usermanager/templates/usermanager/index.html b/lnbits/extensions/usermanager/templates/usermanager/index.html index 446ee51d..6fbe9686 100644 --- a/lnbits/extensions/usermanager/templates/usermanager/index.html +++ b/lnbits/extensions/usermanager/templates/usermanager/index.html @@ -368,6 +368,7 @@ self.users = _.reject(self.users, function (obj) { return obj.id == userId }) + self.getWallets() }) .catch(function (error) { LNbits.utils.notifyApiError(error) diff --git a/lnbits/extensions/usermanager/views.py b/lnbits/extensions/usermanager/views.py index d58a9826..395e0c0b 100644 --- a/lnbits/extensions/usermanager/views.py +++ b/lnbits/extensions/usermanager/views.py @@ -10,4 +10,4 @@ from . import usermanager_ext, usermanager_renderer @usermanager_ext.get("/", response_class=HTMLResponse) async def index(request: Request, user: User = Depends(check_user_exists)): - return await render_template("usermanager/index.html", user=user.dict()) + return usermanager_renderer().TemplateResponse("usermanager/index.html", {"request": request,"user": user.dict()}) diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py index e8a78bf1..caa513c8 100644 --- a/lnbits/extensions/usermanager/views_api.py +++ b/lnbits/extensions/usermanager/views_api.py @@ -50,7 +50,7 @@ async def api_usermanager_user(user_id, wallet: WalletTypeInfo = Depends(get_key # } # ) async def api_usermanager_users_create(data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type)): - user = await create_usermanager_user(**data) + user = await create_usermanager_user(data) full = user.dict() full["wallets"] = [wallet.dict() for wallet in await get_usermanager_users_wallets(user.id)] return full From cace0892f27106e7d3313098a9ca85ab7857cbda Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Tue, 12 Oct 2021 18:54:40 +0100 Subject: [PATCH 12/75] jukebox working(ish) --- lnbits/extensions/jukebox/crud.py | 56 ++++++------- lnbits/extensions/jukebox/models.py | 40 ++++----- lnbits/extensions/jukebox/static/js/index.js | 59 ++++++------- .../extensions/jukebox/static/js/jukebox.js | 14 ---- .../jukebox/templates/jukebox/jukebox.html | 83 ++++++++++--------- lnbits/extensions/jukebox/views.py | 6 +- lnbits/extensions/jukebox/views_api.py | 47 ++++------- .../extensions/tpos/templates/tpos/tpos.html | 21 +++-- 8 files changed, 149 insertions(+), 177 deletions(-) delete mode 100644 lnbits/extensions/jukebox/static/js/jukebox.js diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py index 4e3ba2f1..f7a726a3 100644 --- a/lnbits/extensions/jukebox/crud.py +++ b/lnbits/extensions/jukebox/crud.py @@ -1,22 +1,13 @@ from typing import List, Optional from . import db -from .models import Jukebox, JukeboxPayment +from .models import Jukebox, JukeboxPayment, CreateJukeLinkData from lnbits.helpers import urlsafe_short_hash async def create_jukebox( - inkey: str, - user: str, - wallet: str, - title: str, - price: int, - sp_user: str, - sp_secret: str, - sp_access_token: Optional[str] = "", - sp_refresh_token: Optional[str] = "", - sp_device: Optional[str] = "", - sp_playlists: Optional[str] = "", + data: CreateJukeLinkData, + inkey: Optional[str] = "", ) -> Jukebox: juke_id = urlsafe_short_hash() result = await db.execute( @@ -26,16 +17,16 @@ async def create_jukebox( """, ( juke_id, - user, - title, - wallet, - sp_user, - sp_secret, - sp_access_token, - sp_refresh_token, - sp_device, - sp_playlists, - int(price), + data.user, + data.title, + data.wallet, + data.sp_user, + data.sp_secret, + data.sp_access_token, + data.sp_refresh_token, + data.sp_device, + data.sp_playlists, + data.price, 0, ), ) @@ -44,11 +35,15 @@ async def create_jukebox( return jukebox -async def update_jukebox(juke_id: str, **kwargs) -> Optional[Jukebox]: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute( - f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (*kwargs.values(), juke_id) - ) +async def update_jukebox( + data: CreateJukeLinkData, juke_id: Optional[str] = "" +) -> Optional[Jukebox]: + q = ", ".join([f"{field[0]} = ?" for field in data]) + items = [f"{field[1]}" for field in data] + items.append(juke_id) + print(q) + print(items) + await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items)) row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,)) return Jukebox(**row) if row else None @@ -66,10 +61,13 @@ async def get_jukebox_by_user(user: str) -> Optional[Jukebox]: async def get_jukeboxs(user: str) -> List[Jukebox]: rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,)) for row in rows: - if row.sp_playlists == "": + + if row.sp_playlists == None: + print("cunt") await delete_jukebox(row.id) rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,)) - return [Jukebox.from_row(row) for row in rows] + + return [Jukebox(**row) for row in rows] async def delete_jukebox(juke_id: str): diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py index 8752ae85..6cb49a5e 100644 --- a/lnbits/extensions/jukebox/models.py +++ b/lnbits/extensions/jukebox/models.py @@ -2,6 +2,8 @@ from typing import NamedTuple from sqlite3 import Row from fastapi.param_functions import Query from pydantic.main import BaseModel +from pydantic import BaseModel +from typing import Optional class CreateJukeLinkData(BaseModel): @@ -17,32 +19,24 @@ class CreateJukeLinkData(BaseModel): price: str = Query(None) -class Jukebox(NamedTuple): - id: str - user: str - title: str - wallet: str - inkey: str - sp_user: str - sp_secret: str - sp_access_token: str - sp_refresh_token: str - sp_device: str - sp_playlists: str - price: int - profit: int - - @classmethod - def from_row(cls, row: Row) -> "Jukebox": - return cls(**dict(row)) +class Jukebox(BaseModel): + id: Optional[str] + user: Optional[str] + title: Optional[str] + wallet: Optional[str] + inkey: Optional[str] + sp_user: Optional[str] + sp_secret: Optional[str] + sp_access_token: Optional[str] + sp_refresh_token: Optional[str] + sp_device: Optional[str] + sp_playlists: Optional[str] + price: Optional[int] + profit: Optional[int] -class JukeboxPayment(NamedTuple): +class JukeboxPayment(BaseModel): payment_hash: str juke_id: str song_id: str paid: bool - - @classmethod - def from_row(cls, row: Row) -> "JukeboxPayment": - return cls(**dict(row)) diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js index fc382d71..049b600e 100644 --- a/lnbits/extensions/jukebox/static/js/index.js +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -3,17 +3,25 @@ Vue.component(VueQrcode.name, VueQrcode) var mapJukebox = obj => { - obj._data = _.clone(obj) - obj.sp_id = obj.id - obj.device = obj.sp_device.split('-')[0] - playlists = obj.sp_playlists.split(',') - var i - playlistsar = [] - for (i = 0; i < playlists.length; i++) { - playlistsar.push(playlists[i].split('-')[0]) + if(obj.sp_device){ + obj._data = _.clone(obj) + + obj.sp_id = obj._data.id + obj.device = obj._data.sp_device.split('-')[0] + playlists = obj._data.sp_playlists.split(',') + var i + playlistsar = [] + for (i = 0; i < playlists.length; i++) { + playlistsar.push(playlists[i].split('-')[0]) + } + obj.playlist = playlistsar.join() + console.log(obj) + return obj } - obj.playlist = playlistsar.join() - return obj + else { + return + } + } new Vue({ @@ -79,13 +87,14 @@ new Vue({ var link = _.findWhere(this.JukeboxLinks, {id: linkId}) this.qrCodeDialog.data = _.clone(link) - console.log(this.qrCodeDialog.data) + this.qrCodeDialog.data.url = window.location.protocol + '//' + window.location.host this.qrCodeDialog.show = true }, getJukeboxes() { self = this + LNbits.api .request( 'GET', @@ -93,10 +102,11 @@ new Vue({ self.g.user.wallets[0].adminkey ) .then(function (response) { - self.JukeboxLinks = response.data.map(mapJukebox) - }) - .catch(err => { - LNbits.utils.notifyApiError(err) + self.JukeboxLinks = response.data.map(function (obj) { + + return mapJukebox(obj) + }) + console.log(self.JukeboxLinks) }) }, deleteJukebox(juke_id) { @@ -125,7 +135,6 @@ new Vue({ self = this var link = _.findWhere(self.JukeboxLinks, {id: linkId}) self.jukeboxDialog.data = _.clone(link._data) - console.log(this.jukeboxDialog.data.sp_access_token) self.refreshDevices() self.refreshPlaylists() @@ -145,7 +154,7 @@ new Vue({ submitSpotifyKeys() { self = this self.jukeboxDialog.data.user = self.g.user.id - + LNbits.api .request( 'POST', @@ -193,9 +202,6 @@ new Vue({ if (self.jukeboxDialog.data.sp_access_token) { self.refreshPlaylists() self.refreshDevices() - console.log('this.devices') - console.log(self.devices) - console.log('this.devices') setTimeout(function () { if (self.devices.length < 1 || self.playlists.length < 1) { self.$q.notify({ @@ -259,16 +265,14 @@ new Vue({ }, updateDB() { self = this - console.log(self.jukeboxDialog.data) LNbits.api .request( 'PUT', - '/jukebox/api/v1/jukebox/' + this.jukeboxDialog.data.sp_id, + '/jukebox/api/v1/jukebox/' + self.jukeboxDialog.data.sp_id, self.g.user.wallets[0].adminkey, self.jukeboxDialog.data ) .then(function (response) { - console.log(response.data) if ( self.jukeboxDialog.data.sp_playlists && self.jukeboxDialog.data.sp_devices @@ -307,7 +311,6 @@ new Vue({ responseObj.items[i].name + '-' + responseObj.items[i].id ) } - console.log(self.playlists) } }, refreshPlaylists() { @@ -372,13 +375,6 @@ new Vue({ }, callAuthorizationApi(body) { self = this - console.log( - btoa( - self.jukeboxDialog.data.sp_user + - ':' + - self.jukeboxDialog.data.sp_secret - ) - ) let xhr = new XMLHttpRequest() xhr.open('POST', 'https://accounts.spotify.com/api/token', true) xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') @@ -403,7 +399,6 @@ new Vue({ } }, created() { - console.log(this.g.user.wallets[0]) var getJukeboxes = this.getJukeboxes getJukeboxes() this.selectedWallet = this.g.user.wallets[0] diff --git a/lnbits/extensions/jukebox/static/js/jukebox.js b/lnbits/extensions/jukebox/static/js/jukebox.js deleted file mode 100644 index ddbb2764..00000000 --- a/lnbits/extensions/jukebox/static/js/jukebox.js +++ /dev/null @@ -1,14 +0,0 @@ -/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ - -Vue.component(VueQrcode.name, VueQrcode) - -new Vue({ - el: '#vue', - mixins: [windowMixin], - data() { - return {} - }, - computed: {}, - methods: {}, - created() {} -}) diff --git a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html index cb3ab49d..6a49f7c4 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html +++ b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html @@ -1,4 +1,4 @@ -{% extends "public.html" %} {% block page %} {% raw %} +{% extends "public.html" %} {% block page %}
@@ -9,10 +9,12 @@
+ {% raw %} {{ currentPlay.name }}
{{ currentPlay.artist }}
+ {% endraw %}
@@ -46,7 +48,7 @@ > - {{ item.name }} - ({{ item.artist }}) + {% raw %} {{ item.name }} - ({{ item.artist }}){% endraw %} @@ -54,49 +56,48 @@ - - - - -
-
- -
-
- {{ receive.name }}
- {{ receive.artist }} + + + +
+
+ +
+
+ {% raw %} + {{ receive.name }}
+ {{ receive.artist }} +
+
+
+
+ Play for {% endraw %}{{ price }}sats +
- -
-
- Play for {% endraw %}{{ price }}{% raw %} sats - -
-
-
- - - - - -
- Copy invoice -
-
-
+ + + + + + + +
+ Copy invoice +
+
+
+
-{% endraw %} {% endblock %} {% block scripts %} - - +{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/lnbits/extensions/copilot/templates/copilot/index.html b/lnbits/extensions/copilot/templates/copilot/index.html new file mode 100644 index 00000000..12d7058a --- /dev/null +++ b/lnbits/extensions/copilot/templates/copilot/index.html @@ -0,0 +1,658 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + {% raw %} + New copilot instance + + + + + + +
+
+
Copilots
+
+ +
+ + + + Export to CSV +
+
+ + + + + {% endraw %} + +
+
+
+ +
+ + +
+ {{SITE_TITLE}} StreamCopilot Extension +
+
+ + + {% include "copilot/_api_docs.html" %} + +
+
+ + + + +
+ +
+ +
+ + + + + + + +
+
+ +
+ +
+ + +
+
+ + +
+
+
+
+
+ + + + +
+
+ +
+ +
+ + +
+
+ + +
+
+
+
+
+ + + + +
+
+ +
+ +
+ + +
+
+ + +
+
+
+
+
+ + + +
+ +
+ +
+
+
+ +
+
+
+ Update Copilot + Create Copilot + Cancel +
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + + + +{% endblock %} diff --git a/lnbits/extensions/copilot/templates/copilot/panel.html b/lnbits/extensions/copilot/templates/copilot/panel.html new file mode 100644 index 00000000..904ab104 --- /dev/null +++ b/lnbits/extensions/copilot/templates/copilot/panel.html @@ -0,0 +1,157 @@ +{% extends "public.html" %} {% block page %} +
+ +
+
+
+ +
+
+
+
+ Title: {% raw %} {{ copilot.title }} {% endraw %} +
+
+ +
+
+
+ + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/lnbits/extensions/copilot/views.py b/lnbits/extensions/copilot/views.py new file mode 100644 index 00000000..ef313a61 --- /dev/null +++ b/lnbits/extensions/copilot/views.py @@ -0,0 +1,61 @@ +from quart import g, abort, render_template, jsonify, websocket +from http import HTTPStatus +import httpx +from collections import defaultdict +from lnbits.decorators import check_user_exists, validate_uuids +from . import copilot_ext +from .crud import get_copilot +from quart import g, abort, render_template, jsonify, websocket +from functools import wraps +import trio +import shortuuid +from . import copilot_ext + + +@copilot_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("copilot/index.html", user=g.user) + + +@copilot_ext.route("/cp/") +async def compose(): + return await render_template("copilot/compose.html") + + +@copilot_ext.route("/pn/") +async def panel(): + return await render_template("copilot/panel.html") + + +##################WEBSOCKET ROUTES######################## + +# socket_relay is a list where the control panel or +# lnurl endpoints can leave a message for the compose window + +connected_websockets = defaultdict(set) + + +@copilot_ext.websocket("/ws//") +async def wss(id): + copilot = await get_copilot(id) + if not copilot: + return "", HTTPStatus.FORBIDDEN + global connected_websockets + send_channel, receive_channel = trio.open_memory_channel(0) + connected_websockets[id].add(send_channel) + try: + while True: + data = await receive_channel.receive() + await websocket.send(data) + finally: + connected_websockets[id].remove(send_channel) + + +async def updater(copilot_id, data, comment): + copilot = await get_copilot(copilot_id) + if not copilot: + return + for queue in connected_websockets[copilot_id]: + await queue.send(f"{data + '-' + comment}") diff --git a/lnbits/extensions/copilot/views_api.py b/lnbits/extensions/copilot/views_api.py new file mode 100644 index 00000000..bf3b4eb7 --- /dev/null +++ b/lnbits/extensions/copilot/views_api.py @@ -0,0 +1,109 @@ +import hashlib +from quart import g, jsonify, url_for, websocket +from http import HTTPStatus +import httpx + +from lnbits.core.crud import get_user +from lnbits.decorators import api_check_wallet_key, api_validate_post_request +from .views import updater + +from . import copilot_ext + +from lnbits.extensions.copilot import copilot_ext +from .crud import ( + create_copilot, + update_copilot, + get_copilot, + get_copilots, + delete_copilot, +) + +#######################COPILOT########################## + + +@copilot_ext.route("/api/v1/copilot", methods=["POST"]) +@copilot_ext.route("/api/v1/copilot/", methods=["PUT"]) +@api_check_wallet_key("admin") +@api_validate_post_request( + schema={ + "title": {"type": "string", "empty": False, "required": True}, + "lnurl_toggle": {"type": "integer", "empty": False}, + "wallet": {"type": "string", "empty": False, "required": False}, + "animation1": {"type": "string", "empty": True, "required": False}, + "animation2": {"type": "string", "empty": True, "required": False}, + "animation3": {"type": "string", "empty": True, "required": False}, + "animation1threshold": {"type": "integer", "empty": True, "required": False}, + "animation2threshold": {"type": "integer", "empty": True, "required": False}, + "animation3threshold": {"type": "integer", "empty": True, "required": False}, + "animation1webhook": {"type": "string", "empty": True, "required": False}, + "animation2webhook": {"type": "string", "empty": True, "required": False}, + "animation3webhook": {"type": "string", "empty": True, "required": False}, + "lnurl_title": {"type": "string", "empty": True, "required": False}, + "show_message": {"type": "integer", "empty": True, "required": False}, + "show_ack": {"type": "integer", "empty": True}, + "show_price": {"type": "string", "empty": True}, + } +) +async def api_copilot_create_or_update(copilot_id=None): + if not copilot_id: + copilot = await create_copilot(user=g.wallet.user, **g.data) + return jsonify(copilot._asdict()), HTTPStatus.CREATED + else: + copilot = await update_copilot(copilot_id=copilot_id, **g.data) + return jsonify(copilot._asdict()), HTTPStatus.OK + + +@copilot_ext.route("/api/v1/copilot", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_copilots_retrieve(): + try: + return ( + jsonify( + [{**copilot._asdict()} for copilot in await get_copilots(g.wallet.user)] + ), + HTTPStatus.OK, + ) + except: + return "" + + +@copilot_ext.route("/api/v1/copilot/", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_copilot_retrieve(copilot_id): + copilot = await get_copilot(copilot_id) + if not copilot: + return jsonify({"message": "copilot does not exist"}), HTTPStatus.NOT_FOUND + if not copilot.lnurl_toggle: + return ( + jsonify({**copilot._asdict()}), + HTTPStatus.OK, + ) + return ( + jsonify({**copilot._asdict(), **{"lnurl": copilot.lnurl}}), + HTTPStatus.OK, + ) + + +@copilot_ext.route("/api/v1/copilot/", methods=["DELETE"]) +@api_check_wallet_key("admin") +async def api_copilot_delete(copilot_id): + copilot = await get_copilot(copilot_id) + + if not copilot: + return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND + + await delete_copilot(copilot_id) + + return "", HTTPStatus.NO_CONTENT + + +@copilot_ext.route("/api/v1/copilot/ws///", methods=["GET"]) +async def api_copilot_ws_relay(copilot_id, comment, data): + copilot = await get_copilot(copilot_id) + if not copilot: + return jsonify({"message": "copilot does not exist"}), HTTPStatus.NOT_FOUND + try: + await updater(copilot_id, data, comment) + except: + return "", HTTPStatus.FORBIDDEN + return "", HTTPStatus.OK From aeee469c6435bf67570c92e3967404456c5c9257 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Tue, 12 Oct 2021 20:24:00 +0100 Subject: [PATCH 16/75] converted models --- lnbits/extensions/copilot/__init__.py | 39 ++++++++++++++++++---- lnbits/extensions/copilot/models.py | 47 +++++++++++++++++++++------ lnbits/extensions/copilot/views.py | 47 +++++++++++++++++---------- 3 files changed, 100 insertions(+), 33 deletions(-) diff --git a/lnbits/extensions/copilot/__init__.py b/lnbits/extensions/copilot/__init__.py index b255282b..d14f1565 100644 --- a/lnbits/extensions/copilot/__init__.py +++ b/lnbits/extensions/copilot/__init__.py @@ -1,17 +1,44 @@ -from quart import Blueprint +import asyncio + +from fastapi import APIRouter, FastAPI +from fastapi.staticfiles import StaticFiles +from starlette.routing import Mount + from lnbits.db import Database +from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart db = Database("ext_copilot") -copilot_ext: Blueprint = Blueprint( - "copilot", __name__, static_folder="static", template_folder="templates" + +copilot_static_files = [ + { + "path": "/copilot/static", + "app": StaticFiles(directory="lnbits/extensions/copilot/static"), + "name": "copilot_static", + } +] +copilot_ext: APIRouter = APIRouter( + prefix="/copilot", + tags=["copilot"] + # "lnurlp", __name__, static_folder="static", template_folder="templates" ) + +def copilot_renderer(): + return template_renderer( + [ + "lnbits/extensions/copilot/templates", + ] + ) + + from .views_api import * # noqa from .views import * # noqa +from .tasks import wait_for_paid_invoices from .lnurl import * # noqa -from .tasks import register_listeners -from lnbits.tasks import record_async -copilot_ext.record(record_async(register_listeners)) +def copilot_start(): + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/copilot/models.py b/lnbits/extensions/copilot/models.py index 70d70cf5..db58d35a 100644 --- a/lnbits/extensions/copilot/models.py +++ b/lnbits/extensions/copilot/models.py @@ -1,13 +1,41 @@ -from sqlite3 import Row -from typing import NamedTuple -import time -from quart import url_for -from lnurl import Lnurl, encode as lnurl_encode # type: ignore +import json +from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult +from starlette.requests import Request +from fastapi.param_functions import Query +from typing import Optional, Dict +from lnbits.lnurl import encode as lnurl_encode # type: ignore from lnurl.types import LnurlPayMetadata # type: ignore -from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore +from sqlite3 import Row +from pydantic import BaseModel -class Copilots(NamedTuple): +class CreateCopilots(BaseModel): + id: str = Query(None) + user: str = Query(None) + title: str = Query(None) + lnurl_toggle: int = Query(None) + wallet: str = Query(None) + animation1: str = Query(None) + animation2: str = Query(None) + animation3: str = Query(None) + animation1threshold: int = Query(None) + animation2threshold: int = Query(None) + animation3threshold: int = Query(None) + animation1webhook: str = Query(None) + animation2webhook: str = Query(None) + animation3webhook: str = Query(None) + lnurl_title: str = Query(None) + show_message: int = Query(None) + show_ack: int = Query(None) + show_price: int = Query(None) + amount_made: int = Query(None) + timestamp: int = Query(None) + fullscreen_cam: int = Query(None) + iframe_url: str = Query(None) + success_url: str = Query(None) + + +class Copilots(BaseModel): id: str user: str title: str @@ -35,7 +63,6 @@ class Copilots(NamedTuple): def from_row(cls, row: Row) -> "Copilots": return cls(**dict(row)) - @property - def lnurl(self) -> Lnurl: - url = url_for("copilot.lnurl_response", cp_id=self.id, _external=True) + def lnurl(self, req: Request) -> str: + url = req.url_for("copilot.lnurl_response", link_id=self.id) return lnurl_encode(url) diff --git a/lnbits/extensions/copilot/views.py b/lnbits/extensions/copilot/views.py index ef313a61..aee38a3c 100644 --- a/lnbits/extensions/copilot/views.py +++ b/lnbits/extensions/copilot/views.py @@ -1,32 +1,45 @@ -from quart import g, abort, render_template, jsonify, websocket from http import HTTPStatus import httpx from collections import defaultdict from lnbits.decorators import check_user_exists, validate_uuids -from . import copilot_ext + from .crud import get_copilot -from quart import g, abort, render_template, jsonify, websocket + from functools import wraps -import trio -import shortuuid -from . import copilot_ext + +from lnbits.decorators import check_user_exists + +from . import copilot_ext, copilot_renderer +from fastapi import FastAPI, Request +from fastapi.params import Depends +from fastapi.templating import Jinja2Templates + +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse +from lnbits.core.models import User + +templates = Jinja2Templates(directory="templates") -@copilot_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(): - return await render_template("copilot/index.html", user=g.user) +@copilot_ext.route("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return copilot_renderer().TemplateResponse( + "copilot/index.html", {"request": request, "user": user.dict()} + ) -@copilot_ext.route("/cp/") -async def compose(): - return await render_template("copilot/compose.html") +@copilot_ext.route("/cp/", response_class=HTMLResponse) +async def compose(request: Request): + return copilot_renderer().TemplateResponse( + "copilot/compose.html", {"request": request} + ) -@copilot_ext.route("/pn/") -async def panel(): - return await render_template("copilot/panel.html") +@copilot_ext.route("/pn/", response_class=HTMLResponse) +async def panel(request: Request): + return copilot_renderer().TemplateResponse( + "copilot/panel.html", {"request": request} + ) ##################WEBSOCKET ROUTES######################## From 1346ad12f1c0f2c850d552f987e22ba72079169e Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Tue, 12 Oct 2021 22:34:43 +0100 Subject: [PATCH 17/75] Endpoints changed but still not working --- lnbits/extensions/copilot/__init__.py | 9 +- lnbits/extensions/copilot/crud.py | 74 ++++++-------- lnbits/extensions/copilot/lnurl.py | 73 ++++++++------ lnbits/extensions/copilot/models.py | 2 +- lnbits/extensions/copilot/tasks.py | 37 +++---- lnbits/extensions/copilot/views.py | 52 +++++----- lnbits/extensions/copilot/views_api.py | 134 +++++++++++++------------ lnbits/extensions/jukebox/__init__.py | 2 - lnbits/extensions/jukebox/views.py | 1 + 9 files changed, 181 insertions(+), 203 deletions(-) diff --git a/lnbits/extensions/copilot/__init__.py b/lnbits/extensions/copilot/__init__.py index d14f1565..94d1e74c 100644 --- a/lnbits/extensions/copilot/__init__.py +++ b/lnbits/extensions/copilot/__init__.py @@ -1,16 +1,13 @@ import asyncio - from fastapi import APIRouter, FastAPI from fastapi.staticfiles import StaticFiles from starlette.routing import Mount - from lnbits.db import Database from lnbits.helpers import template_renderer from lnbits.tasks import catch_everything_and_restart db = Database("ext_copilot") - copilot_static_files = [ { "path": "/copilot/static", @@ -18,11 +15,7 @@ copilot_static_files = [ "name": "copilot_static", } ] -copilot_ext: APIRouter = APIRouter( - prefix="/copilot", - tags=["copilot"] - # "lnurlp", __name__, static_folder="static", template_folder="templates" -) +copilot_ext: APIRouter = APIRouter(prefix="/copilot", tags=["copilot"]) def copilot_renderer(): diff --git a/lnbits/extensions/copilot/crud.py b/lnbits/extensions/copilot/crud.py index d083675e..ce4a2804 100644 --- a/lnbits/extensions/copilot/crud.py +++ b/lnbits/extensions/copilot/crud.py @@ -1,36 +1,14 @@ from typing import List, Optional, Union -# from lnbits.db import open_ext_db from . import db -from .models import Copilots - +from .models import Copilots, CreateCopilotData from lnbits.helpers import urlsafe_short_hash -from quart import jsonify - - ###############COPILOTS########################## async def create_copilot( - title: str, - user: str, - lnurl_toggle: Optional[int] = 0, - wallet: Optional[str] = None, - animation1: Optional[str] = None, - animation2: Optional[str] = None, - animation3: Optional[str] = None, - animation1threshold: Optional[int] = None, - animation2threshold: Optional[int] = None, - animation3threshold: Optional[int] = None, - animation1webhook: Optional[str] = None, - animation2webhook: Optional[str] = None, - animation3webhook: Optional[str] = None, - lnurl_title: Optional[str] = None, - show_message: Optional[int] = 0, - show_ack: Optional[int] = 0, - show_price: Optional[str] = None, - amount_made: Optional[int] = None, + data: CreateCopilotData, inkey: Optional[str] = "" ) -> Copilots: copilot_id = urlsafe_short_hash() @@ -60,24 +38,24 @@ async def create_copilot( VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( - copilot_id, - user, - int(lnurl_toggle), - wallet, - title, - animation1, - animation2, - animation3, - animation1threshold, - animation2threshold, - animation3threshold, - animation1webhook, - animation2webhook, - animation3webhook, - lnurl_title, - int(show_message), - int(show_ack), - show_price, + data.copilot_id, + data.user, + int(data.lnurl_toggle), + data.wallet, + data.title, + data.animation1, + data.animation2, + data.animation3, + data.animation1threshold, + data.animation2threshold, + data.animation3threshold, + data.animation1webhook, + data.animation2webhook, + data.animation3webhook, + data.lnurl_title, + int(data.show_message), + int(data.show_ack), + data.show_price, 0, ), ) @@ -89,17 +67,23 @@ async def update_copilot(copilot_id: str, **kwargs) -> Optional[Copilots]: await db.execute( f"UPDATE copilot.copilots SET {q} WHERE id = ?", (*kwargs.values(), copilot_id) ) - row = await db.fetchone("SELECT * FROM copilot.copilots WHERE id = ?", (copilot_id,)) + row = await db.fetchone( + "SELECT * FROM copilot.copilots WHERE id = ?", (copilot_id,) + ) return Copilots.from_row(row) if row else None async def get_copilot(copilot_id: str) -> Copilots: - row = await db.fetchone("SELECT * FROM copilot.copilots WHERE id = ?", (copilot_id,)) + row = await db.fetchone( + "SELECT * FROM copilot.copilots WHERE id = ?", (copilot_id,) + ) return Copilots.from_row(row) if row else None async def get_copilots(user: str) -> List[Copilots]: - rows = await db.fetchall("""SELECT * FROM copilot.copilots WHERE "user" = ?""", (user,)) + rows = await db.fetchall( + """SELECT * FROM copilot.copilots WHERE "user" = ?""", (user,) + ) return [Copilots.from_row(row) for row in rows] diff --git a/lnbits/extensions/copilot/lnurl.py b/lnbits/extensions/copilot/lnurl.py index 0a10e29b..a47421d7 100644 --- a/lnbits/extensions/copilot/lnurl.py +++ b/lnbits/extensions/copilot/lnurl.py @@ -1,23 +1,36 @@ import json import hashlib import math -from quart import jsonify, url_for, request +from fastapi import Request +import hashlib +from http import HTTPStatus + +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse, JSONResponse # type: ignore +import base64 from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore from lnurl.types import LnurlPayMetadata from lnbits.core.services import create_invoice - +from .models import Copilots, CreateCopilotData from . import copilot_ext from .crud import get_copilot +from typing import Optional +from fastapi.params import Depends +from fastapi.param_functions import Query +from .models import CreateJukeLinkData, CreateJukeboxPayment -@copilot_ext.route("/lnurl/", methods=["GET"]) -async def lnurl_response(cp_id): +@copilot_ext.get("/lnurl/{cp_id}", response_class=HTMLResponse) +async def lnurl_response(req: Request, cp_id: str = Query(None)): cp = await get_copilot(cp_id) if not cp: - return jsonify({"status": "ERROR", "reason": "Copilot not found."}) + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Copilot not found", + ) resp = LnurlPayResponse( - callback=url_for("copilot.lnurl_callback", cp_id=cp_id, _external=True), + callback=req.url_for("copilot.lnurl_callback", cp_id=cp_id, _external=True), min_sendable=10000, max_sendable=50000000, metadata=LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]])), @@ -27,42 +40,36 @@ async def lnurl_response(cp_id): if cp.show_message: params["commentAllowed"] = 300 - return jsonify(params) + return params -@copilot_ext.route("/lnurl/cb/", methods=["GET"]) -async def lnurl_callback(cp_id): +@copilot_ext.get("/lnurl/cb/{cp_id}", response_class=HTMLResponse) +async def lnurl_callback( + cp_id: str = Query(None), amount: str = Query(None), comment: str = Query(None) +): cp = await get_copilot(cp_id) if not cp: - return jsonify({"status": "ERROR", "reason": "Copilot not found."}) + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Copilot not found", + ) - amount_received = int(request.args.get("amount")) + amount_received = int(amount) if amount_received < 10000: - return ( - jsonify( - LnurlErrorResponse( - reason=f"Amount {round(amount_received / 1000)} is smaller than minimum 10 sats." - ).dict() - ), - ) + return LnurlErrorResponse( + reason=f"Amount {round(amount_received / 1000)} is smaller than minimum 10 sats." + ).dict() elif amount_received / 1000 > 10000000: - return ( - jsonify( - LnurlErrorResponse( - reason=f"Amount {round(amount_received / 1000)} is greater than maximum 50000." - ).dict() - ), - ) + return LnurlErrorResponse( + reason=f"Amount {round(amount_received / 1000)} is greater than maximum 50000." + ).dict() comment = "" - if request.args.get("comment"): - comment = request.args.get("comment") + if comment: if len(comment or "") > 300: - return jsonify( - LnurlErrorResponse( - reason=f"Got a comment with {len(comment)} characters, but can only accept 300" - ).dict() - ) + return LnurlErrorResponse( + reason=f"Got a comment with {len(comment)} characters, but can only accept 300" + ).dict() if len(comment) < 1: comment = "none" @@ -83,4 +90,4 @@ async def lnurl_callback(cp_id): disposable=False, routes=[], ) - return jsonify(resp.dict()) + return resp.dict() diff --git a/lnbits/extensions/copilot/models.py b/lnbits/extensions/copilot/models.py index db58d35a..230825a0 100644 --- a/lnbits/extensions/copilot/models.py +++ b/lnbits/extensions/copilot/models.py @@ -9,7 +9,7 @@ from sqlite3 import Row from pydantic import BaseModel -class CreateCopilots(BaseModel): +class CreateCopilotData(BaseModel): id: str = Query(None) user: str = Query(None) title: str = Query(None) diff --git a/lnbits/extensions/copilot/tasks.py b/lnbits/extensions/copilot/tasks.py index ff291e9a..77db918f 100644 --- a/lnbits/extensions/copilot/tasks.py +++ b/lnbits/extensions/copilot/tasks.py @@ -1,8 +1,6 @@ -import trio # type: ignore +import asyncio import json import httpx -from quart import g, jsonify, url_for, websocket -from http import HTTPStatus from lnbits.core import db as core_db from lnbits.core.models import Payment @@ -11,16 +9,17 @@ from lnbits.tasks import register_invoice_listener from .crud import get_copilot from .views import updater import shortuuid +from http import HTTPStatus +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse, JSONResponse # type: ignore -async def register_listeners(): - invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2) - register_invoice_listener(invoice_paid_chan_send) - await wait_for_paid_invoices(invoice_paid_chan_recv) +async def wait_for_paid_invoices(): + invoice_queue = asyncio.Queue() + register_invoice_listener(invoice_queue) - -async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): - async for payment in invoice_paid_chan: + while True: + payment = await invoice_queue.get() await on_invoice_paid(payment) @@ -38,9 +37,9 @@ async def on_invoice_paid(payment: Payment) -> None: copilot = await get_copilot(payment.extra.get("copilot", -1)) if not copilot: - return ( - jsonify({"message": "Copilot link link does not exist."}), - HTTPStatus.NOT_FOUND, + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Copilot does not exist", ) if copilot.animation1threshold: if int(payment.amount / 1000) >= copilot.animation1threshold: @@ -74,15 +73,3 @@ async def on_invoice_paid(payment: Payment) -> None: await updater(copilot.id, data, payment.extra.get("comment")) else: await updater(copilot.id, data, "none") - - -async def mark_webhook_sent(payment: Payment, status: int) -> None: - payment.extra["wh_status"] = status - - await core_db.execute( - """ - UPDATE apipayments SET extra = ? - WHERE hash = ? - """, - (json.dumps(payment.extra), payment.payment_hash), - ) diff --git a/lnbits/extensions/copilot/views.py b/lnbits/extensions/copilot/views.py index aee38a3c..274367b3 100644 --- a/lnbits/extensions/copilot/views.py +++ b/lnbits/extensions/copilot/views.py @@ -1,8 +1,8 @@ from http import HTTPStatus import httpx from collections import defaultdict -from lnbits.decorators import check_user_exists, validate_uuids - +from lnbits.decorators import check_user_exists +import asyncio from .crud import get_copilot from functools import wraps @@ -10,13 +10,15 @@ from functools import wraps from lnbits.decorators import check_user_exists from . import copilot_ext, copilot_renderer -from fastapi import FastAPI, Request +from fastapi import FastAPI, Request, WebSocket from fastapi.params import Depends from fastapi.templating import Jinja2Templates - +from fastapi.param_functions import Query from starlette.exceptions import HTTPException -from starlette.responses import HTMLResponse +from starlette.responses import HTMLResponse, JSONResponse # type: ignore from lnbits.core.models import User +import base64 + templates = Jinja2Templates(directory="templates") @@ -50,25 +52,25 @@ async def panel(request: Request): connected_websockets = defaultdict(set) -@copilot_ext.websocket("/ws//") -async def wss(id): - copilot = await get_copilot(id) - if not copilot: - return "", HTTPStatus.FORBIDDEN - global connected_websockets - send_channel, receive_channel = trio.open_memory_channel(0) - connected_websockets[id].add(send_channel) - try: - while True: - data = await receive_channel.receive() - await websocket.send(data) - finally: - connected_websockets[id].remove(send_channel) +# @copilot_ext.websocket("/ws/{id}/") +# async def websocket_endpoint(websocket: WebSocket, id: str = Query(None)): +# copilot = await get_copilot(id) +# if not copilot: +# return "", HTTPStatus.FORBIDDEN +# await websocket.accept() +# invoice_queue = asyncio.Queue() +# connected_websockets[id].add(invoice_queue) +# try: +# while True: +# data = await websocket.receive_text() +# await websocket.send_text(f"Message text was: {data}") +# finally: +# connected_websockets[id].remove(invoice_queue) -async def updater(copilot_id, data, comment): - copilot = await get_copilot(copilot_id) - if not copilot: - return - for queue in connected_websockets[copilot_id]: - await queue.send(f"{data + '-' + comment}") +# async def updater(copilot_id, data, comment): +# copilot = await get_copilot(copilot_id) +# if not copilot: +# return +# for queue in connected_websockets[copilot_id]: +# await queue.send(f"{data + '-' + comment}") diff --git a/lnbits/extensions/copilot/views_api.py b/lnbits/extensions/copilot/views_api.py index bf3b4eb7..9cf2e536 100644 --- a/lnbits/extensions/copilot/views_api.py +++ b/lnbits/extensions/copilot/views_api.py @@ -1,15 +1,29 @@ +from fastapi import Request import hashlib -from quart import g, jsonify, url_for, websocket from http import HTTPStatus -import httpx +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse, JSONResponse # type: ignore +import base64 from lnbits.core.crud import get_user -from lnbits.decorators import api_check_wallet_key, api_validate_post_request +from lnbits.core.services import create_invoice, check_invoice_status +import json +from typing import Optional +from fastapi.params import Depends +from fastapi.param_functions import Query +from .models import Copilots, CreateCopilotData +from lnbits.decorators import ( + WalletAdminKeyChecker, + WalletInvoiceKeyChecker, + api_validate_post_request, + check_user_exists, + WalletTypeInfo, + get_key_type, + api_validate_post_request, +) from .views import updater - +import httpx from . import copilot_ext - -from lnbits.extensions.copilot import copilot_ext from .crud import ( create_copilot, update_copilot, @@ -21,89 +35,81 @@ from .crud import ( #######################COPILOT########################## -@copilot_ext.route("/api/v1/copilot", methods=["POST"]) -@copilot_ext.route("/api/v1/copilot/", methods=["PUT"]) -@api_check_wallet_key("admin") -@api_validate_post_request( - schema={ - "title": {"type": "string", "empty": False, "required": True}, - "lnurl_toggle": {"type": "integer", "empty": False}, - "wallet": {"type": "string", "empty": False, "required": False}, - "animation1": {"type": "string", "empty": True, "required": False}, - "animation2": {"type": "string", "empty": True, "required": False}, - "animation3": {"type": "string", "empty": True, "required": False}, - "animation1threshold": {"type": "integer", "empty": True, "required": False}, - "animation2threshold": {"type": "integer", "empty": True, "required": False}, - "animation3threshold": {"type": "integer", "empty": True, "required": False}, - "animation1webhook": {"type": "string", "empty": True, "required": False}, - "animation2webhook": {"type": "string", "empty": True, "required": False}, - "animation3webhook": {"type": "string", "empty": True, "required": False}, - "lnurl_title": {"type": "string", "empty": True, "required": False}, - "show_message": {"type": "integer", "empty": True, "required": False}, - "show_ack": {"type": "integer", "empty": True}, - "show_price": {"type": "string", "empty": True}, - } -) -async def api_copilot_create_or_update(copilot_id=None): +@copilot_ext.post("/api/v1/copilot", response_class=HTMLResponse) +@copilot_ext.put("/api/v1/copilot/{juke_id}", response_class=HTMLResponse) +async def api_copilot_create_or_update( + data: CreateCopilotData, + copilot_id: str = Query(None), + wallet: WalletTypeInfo = Depends(get_key_type), +): if not copilot_id: - copilot = await create_copilot(user=g.wallet.user, **g.data) - return jsonify(copilot._asdict()), HTTPStatus.CREATED + copilot = await create_copilot(data, user=wallet.wallet.user) + return copilot, HTTPStatus.CREATED else: - copilot = await update_copilot(copilot_id=copilot_id, **g.data) - return jsonify(copilot._asdict()), HTTPStatus.OK + copilot = await update_copilot(data, copilot_id=copilot_id) + return copilot -@copilot_ext.route("/api/v1/copilot", methods=["GET"]) -@api_check_wallet_key("invoice") -async def api_copilots_retrieve(): +@copilot_ext.get("/api/v1/copilot", response_class=HTMLResponse) +async def api_copilots_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)): try: - return ( - jsonify( - [{**copilot._asdict()} for copilot in await get_copilots(g.wallet.user)] - ), - HTTPStatus.OK, - ) + return [{copilot} for copilot in await get_copilots(wallet.wallet.user)] except: return "" -@copilot_ext.route("/api/v1/copilot/", methods=["GET"]) -@api_check_wallet_key("invoice") -async def api_copilot_retrieve(copilot_id): +@copilot_ext.get("/api/v1/copilot/{copilot_id}", response_class=HTMLResponse) +async def api_copilot_retrieve( + copilot_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) +): copilot = await get_copilot(copilot_id) if not copilot: - return jsonify({"message": "copilot does not exist"}), HTTPStatus.NOT_FOUND - if not copilot.lnurl_toggle: - return ( - jsonify({**copilot._asdict()}), - HTTPStatus.OK, + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Copilot not found", ) - return ( - jsonify({**copilot._asdict(), **{"lnurl": copilot.lnurl}}), - HTTPStatus.OK, - ) + if not copilot.lnurl_toggle: + return copilot.dict() + return {**copilot.dict(), **{"lnurl": copilot.lnurl}} -@copilot_ext.route("/api/v1/copilot/", methods=["DELETE"]) -@api_check_wallet_key("admin") -async def api_copilot_delete(copilot_id): +@copilot_ext.delete("/api/v1/copilot/{copilot_id}", response_class=HTMLResponse) +async def api_copilot_delete( + copilot_id: str = Query(None), + wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker()), +): copilot = await get_copilot(copilot_id) if not copilot: - return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Copilot does not exist", + ) await delete_copilot(copilot_id) return "", HTTPStatus.NO_CONTENT -@copilot_ext.route("/api/v1/copilot/ws///", methods=["GET"]) -async def api_copilot_ws_relay(copilot_id, comment, data): +@copilot_ext.get( + "/api/v1/copilot/ws/{copilot_id}/{comment}/{data}", response_class=HTMLResponse +) +async def api_copilot_ws_relay( + copilot_id: str = Query(None), + comment: str = Query(None), + data: str = Query(None), +): copilot = await get_copilot(copilot_id) if not copilot: - return jsonify({"message": "copilot does not exist"}), HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Copilot does not exist", + ) try: await updater(copilot_id, data, comment) except: - return "", HTTPStatus.FORBIDDEN - return "", HTTPStatus.OK + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Not your copilot", + ) + return "" diff --git a/lnbits/extensions/jukebox/__init__.py b/lnbits/extensions/jukebox/__init__.py index f38b0ec7..93e1157a 100644 --- a/lnbits/extensions/jukebox/__init__.py +++ b/lnbits/extensions/jukebox/__init__.py @@ -1,9 +1,7 @@ import asyncio - from fastapi import APIRouter, FastAPI from fastapi.staticfiles import StaticFiles from starlette.routing import Mount - from lnbits.db import Database from lnbits.helpers import template_renderer from lnbits.tasks import catch_everything_and_restart diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py index b0bd0640..230a61e3 100644 --- a/lnbits/extensions/jukebox/views.py +++ b/lnbits/extensions/jukebox/views.py @@ -1,5 +1,6 @@ import json import time + from datetime import datetime from http import HTTPStatus from lnbits.decorators import check_user_exists, WalletTypeInfo, get_key_type From e1b4df869ffb09b1b7b98e99889715d611fab5ec Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Tue, 12 Oct 2021 23:11:10 +0100 Subject: [PATCH 18/75] copilot starts, templates need adjusting --- lnbits/extensions/copilot/lnurl.py | 1 - lnbits/extensions/copilot/tasks.py | 12 +++++++ lnbits/extensions/copilot/views.py | 46 +++++++++++++------------- lnbits/extensions/copilot/views_api.py | 2 +- 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/lnbits/extensions/copilot/lnurl.py b/lnbits/extensions/copilot/lnurl.py index a47421d7..518b07f3 100644 --- a/lnbits/extensions/copilot/lnurl.py +++ b/lnbits/extensions/copilot/lnurl.py @@ -17,7 +17,6 @@ from .crud import get_copilot from typing import Optional from fastapi.params import Depends from fastapi.param_functions import Query -from .models import CreateJukeLinkData, CreateJukeboxPayment @copilot_ext.get("/lnurl/{cp_id}", response_class=HTMLResponse) diff --git a/lnbits/extensions/copilot/tasks.py b/lnbits/extensions/copilot/tasks.py index 77db918f..ea678222 100644 --- a/lnbits/extensions/copilot/tasks.py +++ b/lnbits/extensions/copilot/tasks.py @@ -73,3 +73,15 @@ async def on_invoice_paid(payment: Payment) -> None: await updater(copilot.id, data, payment.extra.get("comment")) else: await updater(copilot.id, data, "none") + + +async def mark_webhook_sent(payment: Payment, status: int) -> None: + payment.extra["wh_status"] = status + + await core_db.execute( + """ + UPDATE apipayments SET extra = ? + WHERE hash = ? + """, + (json.dumps(payment.extra), payment.payment_hash), + ) diff --git a/lnbits/extensions/copilot/views.py b/lnbits/extensions/copilot/views.py index 274367b3..7809c99d 100644 --- a/lnbits/extensions/copilot/views.py +++ b/lnbits/extensions/copilot/views.py @@ -23,21 +23,21 @@ import base64 templates = Jinja2Templates(directory="templates") -@copilot_ext.route("/", response_class=HTMLResponse) +@copilot_ext.get("/", response_class=HTMLResponse) async def index(request: Request, user: User = Depends(check_user_exists)): return copilot_renderer().TemplateResponse( "copilot/index.html", {"request": request, "user": user.dict()} ) -@copilot_ext.route("/cp/", response_class=HTMLResponse) +@copilot_ext.get("/cp/", response_class=HTMLResponse) async def compose(request: Request): return copilot_renderer().TemplateResponse( "copilot/compose.html", {"request": request} ) -@copilot_ext.route("/pn/", response_class=HTMLResponse) +@copilot_ext.get("/pn/", response_class=HTMLResponse) async def panel(request: Request): return copilot_renderer().TemplateResponse( "copilot/panel.html", {"request": request} @@ -52,25 +52,25 @@ async def panel(request: Request): connected_websockets = defaultdict(set) -# @copilot_ext.websocket("/ws/{id}/") -# async def websocket_endpoint(websocket: WebSocket, id: str = Query(None)): -# copilot = await get_copilot(id) -# if not copilot: -# return "", HTTPStatus.FORBIDDEN -# await websocket.accept() -# invoice_queue = asyncio.Queue() -# connected_websockets[id].add(invoice_queue) -# try: -# while True: -# data = await websocket.receive_text() -# await websocket.send_text(f"Message text was: {data}") -# finally: -# connected_websockets[id].remove(invoice_queue) +@copilot_ext.websocket("/ws/{id}/") +async def websocket_endpoint(websocket: WebSocket, id: str = Query(None)): + copilot = await get_copilot(id) + if not copilot: + return "", HTTPStatus.FORBIDDEN + await websocket.accept() + invoice_queue = asyncio.Queue() + connected_websockets[id].add(invoice_queue) + try: + while True: + data = await websocket.receive_text() + await websocket.send_text(f"Message text was: {data}") + finally: + connected_websockets[id].remove(invoice_queue) -# async def updater(copilot_id, data, comment): -# copilot = await get_copilot(copilot_id) -# if not copilot: -# return -# for queue in connected_websockets[copilot_id]: -# await queue.send(f"{data + '-' + comment}") +async def updater(copilot_id, data, comment): + copilot = await get_copilot(copilot_id) + if not copilot: + return + for queue in connected_websockets[copilot_id]: + await queue.send(f"{data + '-' + comment}") diff --git a/lnbits/extensions/copilot/views_api.py b/lnbits/extensions/copilot/views_api.py index 9cf2e536..b256b102 100644 --- a/lnbits/extensions/copilot/views_api.py +++ b/lnbits/extensions/copilot/views_api.py @@ -76,7 +76,7 @@ async def api_copilot_retrieve( @copilot_ext.delete("/api/v1/copilot/{copilot_id}", response_class=HTMLResponse) async def api_copilot_delete( copilot_id: str = Query(None), - wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker()), + wallet: WalletTypeInfo = Depends(get_key_type), ): copilot = await get_copilot(copilot_id) From a0be1a5017f0322f8f7a77dd3183f6283dde5010 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Tue, 12 Oct 2021 23:26:01 +0100 Subject: [PATCH 19/75] Copilot loads --- .../copilot/templates/copilot/_api_docs.html | 12 ++++++------ .../copilot/templates/copilot/index.html | 19 +++++++++---------- lnbits/extensions/copilot/views.py | 1 - 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/lnbits/extensions/copilot/templates/copilot/_api_docs.html b/lnbits/extensions/copilot/templates/copilot/_api_docs.html index d6289be9..9cbb99cb 100644 --- a/lnbits/extensions/copilot/templates/copilot/_api_docs.html +++ b/lnbits/extensions/copilot/templates/copilot/_api_docs.html @@ -35,7 +35,7 @@ <string>, "animation": <string>, "show_message":<string>, "amount": <integer>, "lnurl_title": <string>}' -H "Content-type: application/json" - -H "X-Api-Key: {{g.user.wallets[0].adminkey }}" + -H "X-Api-Key: {{user.wallets[0].adminkey }}"
@@ -63,7 +63,7 @@ "animation": <string>, "show_message":<string>, "amount": <integer>, "lnurl_title": <string>}' -H "Content-type: application/json" -H "X-Api-Key: - {{g.user.wallets[0].adminkey }}" + {{user.wallets[0].adminkey }}" @@ -88,7 +88,7 @@
Curl example
curl -X GET {{ request.url_root }}api/v1/copilot/<copilot_id> - -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" + -H "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -111,7 +111,7 @@
Curl example
curl -X GET {{ request.url_root }}api/v1/copilots -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" + user.wallets[0].inkey }}" @@ -137,7 +137,7 @@ curl -X DELETE {{ request.url_root }}api/v1/copilot/<copilot_id> -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" + user.wallets[0].adminkey }}" @@ -163,7 +163,7 @@ curl -X GET {{ request.url_root }}/api/v1/copilot/ws/<string, copilot_id>/<string, comment>/<string, gif name> -H - "X-Api-Key: {{ g.user.wallets[0].adminkey }}" + "X-Api-Key: {{ user.wallets[0].adminkey }}" diff --git a/lnbits/extensions/copilot/templates/copilot/index.html b/lnbits/extensions/copilot/templates/copilot/index.html index 12d7058a..c5f96fe2 100644 --- a/lnbits/extensions/copilot/templates/copilot/index.html +++ b/lnbits/extensions/copilot/templates/copilot/index.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% from "macros.jinja" import window_vars with context %} {% block page %} +
@@ -384,8 +385,6 @@
{% endblock %} {% block scripts %} {{ window_vars(user) }} - - + +{% endblock %} diff --git a/lnbits/extensions/satsdice/views.py b/lnbits/extensions/satsdice/views.py new file mode 100644 index 00000000..2e30bc39 --- /dev/null +++ b/lnbits/extensions/satsdice/views.py @@ -0,0 +1,128 @@ +from quart import g, abort, render_template +from http import HTTPStatus +import pyqrcode +from io import BytesIO +from lnbits.decorators import check_user_exists, validate_uuids +from lnbits.core.crud import get_user, get_standalone_payment +from lnbits.core.services import check_invoice_status +import random + +from . import satsdice_ext +from .crud import ( + get_satsdice_pay, + update_satsdice_payment, + get_satsdice_payment, + create_satsdice_withdraw, + get_satsdice_withdraw, +) + + +@satsdice_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("satsdice/index.html", user=g.user) + + +@satsdice_ext.route("/") +async def display(link_id): + link = await get_satsdice_pay(link_id) or abort( + HTTPStatus.NOT_FOUND, "satsdice link does not exist." + ) + return await render_template( + "satsdice/display.html", + chance=link.chance, + multiplier=link.multiplier, + lnurl=link.lnurl, + unique=True, + ) + + +@satsdice_ext.route("/win//") +async def displaywin(link_id, payment_hash): + satsdicelink = await get_satsdice_pay(link_id) or abort( + HTTPStatus.NOT_FOUND, "satsdice link does not exist." + ) + withdrawLink = await get_satsdice_withdraw(payment_hash) + + if withdrawLink: + return await render_template( + "satsdice/displaywin.html", + value=withdrawLink.value, + chance=satsdicelink.chance, + multiplier=satsdicelink.multiplier, + lnurl=withdrawLink.lnurl, + paid=False, + lost=False, + ) + + payment = await get_standalone_payment(payment_hash) or abort( + HTTPStatus.NOT_FOUND, "satsdice link does not exist." + ) + + if payment.pending == 1: + await check_invoice_status(payment.wallet_id, payment_hash) + payment = await get_standalone_payment(payment_hash) or abort( + HTTPStatus.NOT_FOUND, "satsdice link does not exist." + ) + if payment.pending == 1: + print("pending") + return await render_template( + "satsdice/error.html", link=satsdicelink.id, paid=False, lost=False + ) + + await update_satsdice_payment(payment_hash, paid=1) + + paylink = await get_satsdice_payment(payment_hash) or abort( + HTTPStatus.NOT_FOUND, "satsdice link does not exist." + ) + + if paylink.lost == 1: + print("lost") + return await render_template( + "satsdice/error.html", link=satsdicelink.id, paid=False, lost=True + ) + rand = random.randint(0, 100) + chance = satsdicelink.chance + if rand > chance: + await update_satsdice_payment(payment_hash, lost=1) + return await render_template( + "satsdice/error.html", link=satsdicelink.id, paid=False, lost=True + ) + + withdrawLink = await create_satsdice_withdraw( + payment_hash=payment_hash, + satsdice_pay=satsdicelink.id, + value=paylink.value * satsdicelink.multiplier, + used=0, + ) + + return await render_template( + "satsdice/displaywin.html", + value=withdrawLink.value, + chance=satsdicelink.chance, + multiplier=satsdicelink.multiplier, + lnurl=withdrawLink.lnurl, + paid=False, + lost=False, + ) + + +@satsdice_ext.route("/img/") +async def img(link_id): + link = await get_satsdice_pay(link_id) or abort( + HTTPStatus.NOT_FOUND, "satsdice link does not exist." + ) + qr = pyqrcode.create(link.lnurl) + stream = BytesIO() + qr.svg(stream, scale=3) + return ( + stream.getvalue(), + 200, + { + "Content-Type": "image/svg+xml", + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0", + }, + ) diff --git a/lnbits/extensions/satsdice/views_api.py b/lnbits/extensions/satsdice/views_api.py new file mode 100644 index 00000000..90e7a8c2 --- /dev/null +++ b/lnbits/extensions/satsdice/views_api.py @@ -0,0 +1,265 @@ +from quart import g, jsonify, request +from http import HTTPStatus +from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore + +from lnbits.core.crud import get_user +from lnbits.decorators import api_check_wallet_key, api_validate_post_request + +from . import satsdice_ext +from .crud import ( + create_satsdice_pay, + get_satsdice_pay, + get_satsdice_pays, + update_satsdice_pay, + delete_satsdice_pay, + create_satsdice_withdraw, + get_satsdice_withdraw, + get_satsdice_withdraws, + update_satsdice_withdraw, + delete_satsdice_withdraw, + create_withdraw_hash_check, +) + +################LNURL pay + + +@satsdice_ext.route("/api/v1/links", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_links(): + wallet_ids = [g.wallet.id] + + if "all_wallets" in request.args: + wallet_ids = (await get_user(g.wallet.user)).wallet_ids + + try: + return ( + jsonify( + [ + {**link._asdict(), **{"lnurl": link.lnurl}} + for link in await get_satsdice_pays(wallet_ids) + ] + ), + HTTPStatus.OK, + ) + except LnurlInvalidUrl: + return ( + jsonify( + { + "message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor." + } + ), + HTTPStatus.UPGRADE_REQUIRED, + ) + + +@satsdice_ext.route("/api/v1/links/", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_link_retrieve(link_id): + link = await get_satsdice_pay(link_id) + + if not link: + return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND + + if link.wallet != g.wallet.id: + return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN + + return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), HTTPStatus.OK + + +@satsdice_ext.route("/api/v1/links", methods=["POST"]) +@satsdice_ext.route("/api/v1/links/", methods=["PUT"]) +@api_check_wallet_key("invoice") +@api_validate_post_request( + schema={ + "title": {"type": "string", "empty": False, "required": True}, + "base_url": {"type": "string", "empty": False, "required": True}, + "min_bet": {"type": "number", "required": True}, + "max_bet": {"type": "number", "required": True}, + "multiplier": {"type": "number", "required": True}, + "chance": {"type": "float", "required": True}, + "haircut": {"type": "number", "required": True}, + } +) +async def api_link_create_or_update(link_id=None): + if g.data["min_bet"] > g.data["max_bet"]: + return jsonify({"message": "Min is greater than max."}), HTTPStatus.BAD_REQUEST + if link_id: + link = await get_satsdice_pay(link_id) + + if not link: + return ( + jsonify({"message": "Satsdice does not exist."}), + HTTPStatus.NOT_FOUND, + ) + + if link.wallet != g.wallet.id: + return ( + jsonify({"message": "Come on, seriously, this isn't your satsdice!"}), + HTTPStatus.FORBIDDEN, + ) + + link = await update_satsdice_pay(link_id, **g.data) + else: + link = await create_satsdice_pay(wallet_id=g.wallet.id, **g.data) + + return ( + jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), + HTTPStatus.OK if link_id else HTTPStatus.CREATED, + ) + + +@satsdice_ext.route("/api/v1/links/", methods=["DELETE"]) +@api_check_wallet_key("invoice") +async def api_link_delete(link_id): + link = await get_satsdice_pay(link_id) + + if not link: + return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND + + if link.wallet != g.wallet.id: + return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN + + await delete_satsdice_pay(link_id) + + return "", HTTPStatus.NO_CONTENT + + +##########LNURL withdraw + + +@satsdice_ext.route("/api/v1/withdraws", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_withdraws(): + wallet_ids = [g.wallet.id] + + if "all_wallets" in request.args: + wallet_ids = (await get_user(g.wallet.user)).wallet_ids + try: + return ( + jsonify( + [ + { + **withdraw._asdict(), + **{"lnurl": withdraw.lnurl}, + } + for withdraw in await get_satsdice_withdraws(wallet_ids) + ] + ), + HTTPStatus.OK, + ) + except LnurlInvalidUrl: + return ( + jsonify( + { + "message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor." + } + ), + HTTPStatus.UPGRADE_REQUIRED, + ) + + +@satsdice_ext.route("/api/v1/withdraws/", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_withdraw_retrieve(withdraw_id): + withdraw = await get_satsdice_withdraw(withdraw_id, 0) + + if not withdraw: + return ( + jsonify({"message": "satsdice withdraw does not exist."}), + HTTPStatus.NOT_FOUND, + ) + + if withdraw.wallet != g.wallet.id: + return ( + jsonify({"message": "Not your satsdice withdraw."}), + HTTPStatus.FORBIDDEN, + ) + + return jsonify({**withdraw._asdict(), **{"lnurl": withdraw.lnurl}}), HTTPStatus.OK + + +@satsdice_ext.route("/api/v1/withdraws", methods=["POST"]) +@satsdice_ext.route("/api/v1/withdraws/", methods=["PUT"]) +@api_check_wallet_key("admin") +@api_validate_post_request( + schema={ + "title": {"type": "string", "empty": False, "required": True}, + "min_satsdiceable": {"type": "integer", "min": 1, "required": True}, + "max_satsdiceable": {"type": "integer", "min": 1, "required": True}, + "uses": {"type": "integer", "min": 1, "required": True}, + "wait_time": {"type": "integer", "min": 1, "required": True}, + "is_unique": {"type": "boolean", "required": True}, + } +) +async def api_withdraw_create_or_update(withdraw_id=None): + if g.data["max_satsdiceable"] < g.data["min_satsdiceable"]: + return ( + jsonify( + { + "message": "`max_satsdiceable` needs to be at least `min_satsdiceable`." + } + ), + HTTPStatus.BAD_REQUEST, + ) + + usescsv = "" + for i in range(g.data["uses"]): + if g.data["is_unique"]: + usescsv += "," + str(i + 1) + else: + usescsv += "," + str(1) + usescsv = usescsv[1:] + + if withdraw_id: + withdraw = await get_satsdice_withdraw(withdraw_id, 0) + if not withdraw: + return ( + jsonify({"message": "satsdice withdraw does not exist."}), + HTTPStatus.NOT_FOUND, + ) + if withdraw.wallet != g.wallet.id: + return ( + jsonify({"message": "Not your satsdice withdraw."}), + HTTPStatus.FORBIDDEN, + ) + withdraw = await update_satsdice_withdraw( + withdraw_id, **g.data, usescsv=usescsv, used=0 + ) + else: + withdraw = await create_satsdice_withdraw( + wallet_id=g.wallet.id, **g.data, usescsv=usescsv + ) + + return ( + jsonify({**withdraw._asdict(), **{"lnurl": withdraw.lnurl}}), + HTTPStatus.OK if withdraw_id else HTTPStatus.CREATED, + ) + + +@satsdice_ext.route("/api/v1/withdraws/", methods=["DELETE"]) +@api_check_wallet_key("admin") +async def api_withdraw_delete(withdraw_id): + withdraw = await get_satsdice_withdraw(withdraw_id) + + if not withdraw: + return ( + jsonify({"message": "satsdice withdraw does not exist."}), + HTTPStatus.NOT_FOUND, + ) + + if withdraw.wallet != g.wallet.id: + return ( + jsonify({"message": "Not your satsdice withdraw."}), + HTTPStatus.FORBIDDEN, + ) + + await delete_satsdice_withdraw(withdraw_id) + + return "", HTTPStatus.NO_CONTENT + + +@satsdice_ext.route("/api/v1/withdraws//", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_withdraw_hash_retrieve(the_hash, lnurl_id): + hashCheck = await get_withdraw_hash_check(the_hash, lnurl_id) + return jsonify(hashCheck), HTTPStatus.OK From 33cfbe87bb1b0cd4bf3e86c8fae09797e6199bd8 Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 13 Oct 2021 21:51:21 +0100 Subject: [PATCH 27/75] views converted --- lnbits/extensions/jukebox/views_api.py | 1 + lnbits/extensions/satsdice/__init__.py | 26 +- lnbits/extensions/satsdice/crud.py | 61 ++-- lnbits/extensions/satsdice/lnurl.py | 1 - lnbits/extensions/satsdice/models.py | 38 ++- .../satsdice/templates/satsdice/index.html | 3 +- lnbits/extensions/satsdice/views.py | 71 ++--- lnbits/extensions/satsdice/views_api.py | 260 +++++++++--------- 8 files changed, 252 insertions(+), 209 deletions(-) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 233873ef..ee2820db 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -1,4 +1,5 @@ from fastapi import Request + from http import HTTPStatus from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse, JSONResponse # type: ignore diff --git a/lnbits/extensions/satsdice/__init__.py b/lnbits/extensions/satsdice/__init__.py index b991b135..b70b570e 100644 --- a/lnbits/extensions/satsdice/__init__.py +++ b/lnbits/extensions/satsdice/__init__.py @@ -1,14 +1,30 @@ -from quart import Blueprint +import asyncio +from fastapi import APIRouter, FastAPI +from fastapi.staticfiles import StaticFiles +from starlette.routing import Mount from lnbits.db import Database +from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart -db = Database("ext_satsdice") +db = Database("satsdice_ext") + +satsdice_ext: APIRouter = APIRouter(prefix="/satsdice", tags=["satsdice"]) -satsdice_ext: Blueprint = Blueprint( - "satsdice", __name__, static_folder="static", template_folder="templates" -) +def satsdice_renderer(): + return template_renderer( + [ + "lnbits/extensions/satsdice/templates", + ] + ) from .views_api import * # noqa from .views import * # noqa from .lnurl import * # noqa +from .tasks import wait_for_paid_invoices + + +def satsdice_start(): + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/satsdice/crud.py b/lnbits/extensions/satsdice/crud.py index 78983142..c5434b39 100644 --- a/lnbits/extensions/satsdice/crud.py +++ b/lnbits/extensions/satsdice/crud.py @@ -1,23 +1,20 @@ from datetime import datetime from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash - +from typing import List, Optional from . import db -from .models import satsdiceWithdraw, HashCheck, satsdiceLink, satsdicePayment - -##################SATSDICE PAY LINKS +from .models import ( + satsdiceWithdraw, + HashCheck, + satsdiceLink, + satsdicePayment, + CreateSatsDiceLink, +) +from lnbits.helpers import urlsafe_short_hash async def create_satsdice_pay( - *, - wallet_id: str, - title: str, - base_url: str, - min_bet: str, - max_bet: str, - multiplier: int = 0, - chance: float = 0, - haircut: int = 0, + data: CreateSatsDiceLink, ) -> satsdiceLink: satsdice_id = urlsafe_short_hash() await db.execute( @@ -41,14 +38,14 @@ async def create_satsdice_pay( """, ( satsdice_id, - wallet_id, - title, - base_url, - min_bet, - max_bet, - multiplier, - chance, - haircut, + data.wallet_id, + data.title, + data.base_url, + data.min_bet, + data.max_bet, + data.multiplier, + data.chance, + data.haircut, int(datetime.now().timestamp()), ), ) @@ -111,9 +108,7 @@ async def delete_satsdice_pay(link_id: int) -> None: ##################SATSDICE PAYMENT LINKS -async def create_satsdice_payment( - *, satsdice_pay: str, value: int, payment_hash: str -) -> satsdicePayment: +async def create_satsdice_payment(data: CreateSatsDicePayment) -> satsdicePayment: await db.execute( """ INSERT INTO satsdice.satsdice_payment ( @@ -126,9 +121,9 @@ async def create_satsdice_payment( VALUES (?, ?, ?, ?, ?) """, ( - payment_hash, - satsdice_pay, - value, + data.payment_hash, + data.satsdice_pay, + data.value, False, False, ), @@ -165,9 +160,7 @@ async def update_satsdice_payment( ##################SATSDICE WITHDRAW LINKS -async def create_satsdice_withdraw( - *, payment_hash: str, satsdice_pay: str, value: int, used: int -) -> satsdiceWithdraw: +async def create_satsdice_withdraw(data: CreateSatsDiceWithdraw) -> satsdiceWithdraw: await db.execute( """ INSERT INTO satsdice.satsdice_withdraw ( @@ -182,13 +175,13 @@ async def create_satsdice_withdraw( VALUES (?, ?, ?, ?, ?, ?, ?) """, ( - payment_hash, - satsdice_pay, - value, + data.payment_hash, + data.satsdice_pay, + data.value, urlsafe_short_hash(), urlsafe_short_hash(), int(datetime.now().timestamp()), - used, + data.used, ), ) withdraw = await get_satsdice_withdraw(payment_hash, 0) diff --git a/lnbits/extensions/satsdice/lnurl.py b/lnbits/extensions/satsdice/lnurl.py index 1548de98..81c2a4a2 100644 --- a/lnbits/extensions/satsdice/lnurl.py +++ b/lnbits/extensions/satsdice/lnurl.py @@ -3,7 +3,6 @@ import hashlib import math from http import HTTPStatus from datetime import datetime -from quart import jsonify, url_for, request from lnbits.core.services import pay_invoice, create_invoice from lnbits.utils.exchange_rates import get_fiat_rate_satoshis diff --git a/lnbits/extensions/satsdice/models.py b/lnbits/extensions/satsdice/models.py index 5fe732dd..d77f8dc4 100644 --- a/lnbits/extensions/satsdice/models.py +++ b/lnbits/extensions/satsdice/models.py @@ -1,11 +1,14 @@ import json -from quart import url_for from lnurl import Lnurl, LnurlWithdrawResponse, encode as lnurl_encode # type: ignore from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult from lnurl.types import LnurlPayMetadata # type: ignore from sqlite3 import Row from typing import NamedTuple, Optional, Dict import shortuuid # type: ignore +from fastapi.param_functions import Query +from pydantic.main import BaseModel +from pydantic import BaseModel +from typing import Optional class satsdiceLink(NamedTuple): @@ -120,3 +123,36 @@ class HashCheck(NamedTuple): @classmethod def from_row(cls, row: Row) -> "Hash": return cls(**dict(row)) + + +class CreateSatsDiceLink(BaseModel): + wallet_id: str = Query(None) + title: str = Query(None) + base_url: str = Query(None) + min_bet: str = Query(None) + max_bet: str = Query(None) + multiplier: int = Query(0) + chance: float = Query(0) + haircut: int = Query(0) + + +class CreateSatsDicePayment(BaseModel): + satsdice_pay: str = Query(None) + value: int = Query(0) + payment_hash: str = Query(None) + + +class CreateSatsDiceWithdraw(BaseModel): + payment_hash: str = Query(None) + satsdice_pay: str = Query(None) + value: int = Query(0) + used: int = Query(0) + + +class CreateSatsDiceWithdraws(BaseModel): + title: str = Query(None) + min_satsdiceable: int = Query(0) + max_satsdiceable: int = Query(0) + uses: int = Query(0) + wait_time: str = Query(None) + is_unique: bool = Query(False) diff --git a/lnbits/extensions/satsdice/templates/satsdice/index.html b/lnbits/extensions/satsdice/templates/satsdice/index.html index 3e8573b8..d92d43be 100644 --- a/lnbits/extensions/satsdice/templates/satsdice/index.html +++ b/lnbits/extensions/satsdice/templates/satsdice/index.html @@ -262,8 +262,7 @@
{% endblock %} {% block scripts %} {{ window_vars(user) }} - - + + + +{% endblock %} diff --git a/lnbits/extensions/satspay/templates/satspay/index.html b/lnbits/extensions/satspay/templates/satspay/index.html new file mode 100644 index 00000000..f3566c7c --- /dev/null +++ b/lnbits/extensions/satspay/templates/satspay/index.html @@ -0,0 +1,555 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + {% raw %} + New charge + + + + + + +
+
+
Charges
+
+ +
+ + + + Export to CSV +
+
+ + + + + {% endraw %} + +
+
+
+ +
+ + +
+ {{SITE_TITLE}} satspay Extension +
+
+ + + {% include "satspay/_api_docs.html" %} + +
+
+ + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ + + Watch-Only extension MUST be activated and have a wallet + + +
+
+
+
+ +
+
+
+ +
+ +
+ + + +
+ Create Charge + Cancel +
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + + + +{% endblock %} diff --git a/lnbits/extensions/satspay/views.py b/lnbits/extensions/satspay/views.py new file mode 100644 index 00000000..2c99a925 --- /dev/null +++ b/lnbits/extensions/satspay/views.py @@ -0,0 +1,22 @@ +from quart import g, abort, render_template, jsonify +from http import HTTPStatus + +from lnbits.decorators import check_user_exists, validate_uuids + +from . import satspay_ext +from .crud import get_charge + + +@satspay_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("satspay/index.html", user=g.user) + + +@satspay_ext.route("/") +async def display(charge_id): + charge = await get_charge(charge_id) or abort( + HTTPStatus.NOT_FOUND, "Charge link does not exist." + ) + return await render_template("satspay/display.html", charge=charge) diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py new file mode 100644 index 00000000..9440312a --- /dev/null +++ b/lnbits/extensions/satspay/views_api.py @@ -0,0 +1,157 @@ +import hashlib +from quart import g, jsonify, url_for +from http import HTTPStatus +import httpx + + +from lnbits.core.crud import get_user +from lnbits.decorators import api_check_wallet_key, api_validate_post_request + +from lnbits.extensions.satspay import satspay_ext +from .crud import ( + create_charge, + update_charge, + get_charge, + get_charges, + delete_charge, + check_address_balance, +) + +#############################CHARGES########################## + + +@satspay_ext.route("/api/v1/charge", methods=["POST"]) +@satspay_ext.route("/api/v1/charge/", methods=["PUT"]) +@api_check_wallet_key("admin") +@api_validate_post_request( + schema={ + "onchainwallet": {"type": "string"}, + "lnbitswallet": {"type": "string"}, + "description": {"type": "string", "empty": False, "required": True}, + "webhook": {"type": "string"}, + "completelink": {"type": "string"}, + "completelinktext": {"type": "string"}, + "time": {"type": "integer", "min": 1, "required": True}, + "amount": {"type": "integer", "min": 1, "required": True}, + } +) +async def api_charge_create_or_update(charge_id=None): + if not charge_id: + charge = await create_charge(user=g.wallet.user, **g.data) + return jsonify(charge._asdict()), HTTPStatus.CREATED + else: + charge = await update_charge(charge_id=charge_id, **g.data) + return jsonify(charge._asdict()), HTTPStatus.OK + + +@satspay_ext.route("/api/v1/charges", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_charges_retrieve(): + try: + return ( + jsonify( + [ + { + **charge._asdict(), + **{"time_elapsed": charge.time_elapsed}, + **{"paid": charge.paid}, + } + for charge in await get_charges(g.wallet.user) + ] + ), + HTTPStatus.OK, + ) + except: + return "" + + +@satspay_ext.route("/api/v1/charge/", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_charge_retrieve(charge_id): + charge = await get_charge(charge_id) + + if not charge: + return jsonify({"message": "charge does not exist"}), HTTPStatus.NOT_FOUND + + return ( + jsonify( + { + **charge._asdict(), + **{"time_elapsed": charge.time_elapsed}, + **{"paid": charge.paid}, + } + ), + HTTPStatus.OK, + ) + + +@satspay_ext.route("/api/v1/charge/", methods=["DELETE"]) +@api_check_wallet_key("invoice") +async def api_charge_delete(charge_id): + charge = await get_charge(charge_id) + + if not charge: + return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND + + await delete_charge(charge_id) + + return "", HTTPStatus.NO_CONTENT + + +#############################BALANCE########################## + + +@satspay_ext.route("/api/v1/charges/balance/", methods=["GET"]) +async def api_charges_balance(charge_id): + + charge = await check_address_balance(charge_id) + + if not charge: + return jsonify({"message": "charge does not exist"}), HTTPStatus.NOT_FOUND + if charge.paid and charge.webhook: + async with httpx.AsyncClient() as client: + try: + r = await client.post( + charge.webhook, + json={ + "id": charge.id, + "description": charge.description, + "onchainaddress": charge.onchainaddress, + "payment_request": charge.payment_request, + "payment_hash": charge.payment_hash, + "time": charge.time, + "amount": charge.amount, + "balance": charge.balance, + "paid": charge.paid, + "timestamp": charge.timestamp, + "completelink": charge.completelink, + }, + timeout=40, + ) + except AssertionError: + charge.webhook = None + return jsonify(charge._asdict()), HTTPStatus.OK + + +#############################MEMPOOL########################## + + +@satspay_ext.route("/api/v1/mempool", methods=["PUT"]) +@api_check_wallet_key("invoice") +@api_validate_post_request( + schema={ + "endpoint": {"type": "string", "empty": False, "required": True}, + } +) +async def api_update_mempool(): + mempool = await update_mempool(user=g.wallet.user, **g.data) + return jsonify(mempool._asdict()), HTTPStatus.OK + + +@satspay_ext.route("/api/v1/mempool", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_get_mempool(): + mempool = await get_mempool(g.wallet.user) + if not mempool: + mempool = await create_mempool(user=g.wallet.user) + return jsonify(mempool._asdict()), HTTPStatus.OK From ec89244d7faec303c696bb16501badba6eb96cbd Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Thu, 14 Oct 2021 11:45:30 +0100 Subject: [PATCH 30/75] whatchonly ext added --- lnbits/extensions/watchonly/README.md | 19 + lnbits/extensions/watchonly/__init__.py | 13 + lnbits/extensions/watchonly/config.json | 8 + lnbits/extensions/watchonly/crud.py | 212 ++++++ lnbits/extensions/watchonly/migrations.py | 36 + lnbits/extensions/watchonly/models.py | 35 + .../templates/watchonly/_api_docs.html | 244 +++++++ .../watchonly/templates/watchonly/index.html | 649 ++++++++++++++++++ lnbits/extensions/watchonly/views.py | 22 + lnbits/extensions/watchonly/views_api.py | 138 ++++ 10 files changed, 1376 insertions(+) create mode 100644 lnbits/extensions/watchonly/README.md create mode 100644 lnbits/extensions/watchonly/__init__.py create mode 100644 lnbits/extensions/watchonly/config.json create mode 100644 lnbits/extensions/watchonly/crud.py create mode 100644 lnbits/extensions/watchonly/migrations.py create mode 100644 lnbits/extensions/watchonly/models.py create mode 100644 lnbits/extensions/watchonly/templates/watchonly/_api_docs.html create mode 100644 lnbits/extensions/watchonly/templates/watchonly/index.html create mode 100644 lnbits/extensions/watchonly/views.py create mode 100644 lnbits/extensions/watchonly/views_api.py diff --git a/lnbits/extensions/watchonly/README.md b/lnbits/extensions/watchonly/README.md new file mode 100644 index 00000000..d93f7162 --- /dev/null +++ b/lnbits/extensions/watchonly/README.md @@ -0,0 +1,19 @@ +# Watch Only wallet + +## Monitor an onchain wallet and generate addresses for onchain payments + +Monitor an extended public key and generate deterministic fresh public keys with this simple watch only wallet. Invoice payments can also be generated, both through a publically shareable page and API. + +1. Start by clicking "NEW WALLET"\ + ![new wallet](https://i.imgur.com/vgbAB7c.png) +2. Fill the requested fields: + - give the wallet a name + - paste an Extended Public Key (xpub, ypub, zpub) + - click "CREATE WATCH-ONLY WALLET"\ + ![fill wallet form](https://i.imgur.com/UVoG7LD.png) +3. You can then access your onchain addresses\ + ![get address](https://i.imgur.com/zkxTQ6l.png) +4. You can then generate bitcoin onchain adresses from LNbits\ + ![onchain address](https://i.imgur.com/4KVSSJn.png) + +You can now use this wallet on the LNBits [SatsPayServer](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/satspay/README.md) extension diff --git a/lnbits/extensions/watchonly/__init__.py b/lnbits/extensions/watchonly/__init__.py new file mode 100644 index 00000000..b8df3197 --- /dev/null +++ b/lnbits/extensions/watchonly/__init__.py @@ -0,0 +1,13 @@ +from quart import Blueprint +from lnbits.db import Database + +db = Database("ext_watchonly") + + +watchonly_ext: Blueprint = Blueprint( + "watchonly", __name__, static_folder="static", template_folder="templates" +) + + +from .views_api import * # noqa +from .views import * # noqa diff --git a/lnbits/extensions/watchonly/config.json b/lnbits/extensions/watchonly/config.json new file mode 100644 index 00000000..48c19ef0 --- /dev/null +++ b/lnbits/extensions/watchonly/config.json @@ -0,0 +1,8 @@ +{ + "name": "Watch Only", + "short_description": "Onchain watch only wallets", + "icon": "visibility", + "contributors": [ + "arcbtc" + ] +} diff --git a/lnbits/extensions/watchonly/crud.py b/lnbits/extensions/watchonly/crud.py new file mode 100644 index 00000000..bd301eb4 --- /dev/null +++ b/lnbits/extensions/watchonly/crud.py @@ -0,0 +1,212 @@ +from typing import List, Optional + +from . import db +from .models import Wallets, Addresses, Mempool + +from lnbits.helpers import urlsafe_short_hash + +from embit.descriptor import Descriptor, Key # type: ignore +from embit.descriptor.arguments import AllowedDerivation # type: ignore +from embit.networks import NETWORKS # type: ignore + + +##########################WALLETS#################### + + +def detect_network(k): + version = k.key.version + for network_name in NETWORKS: + net = NETWORKS[network_name] + # not found in this network + if version in [net["xpub"], net["ypub"], net["zpub"], net["Zpub"], net["Ypub"]]: + return net + + +def parse_key(masterpub: str): + """Parses masterpub or descriptor and returns a tuple: (Descriptor, network) + To create addresses use descriptor.derive(num).address(network=network) + """ + network = None + # probably a single key + if "(" not in masterpub: + k = Key.from_string(masterpub) + if not k.is_extended: + raise ValueError("The key is not a master public key") + if k.is_private: + raise ValueError("Private keys are not allowed") + # check depth + if k.key.depth != 3: + raise ValueError( + "Non-standard depth. Only bip44, bip49 and bip84 are supported with bare xpubs. For custom derivation paths use descriptors." + ) + # if allowed derivation is not provided use default /{0,1}/* + if k.allowed_derivation is None: + k.allowed_derivation = AllowedDerivation.default() + # get version bytes + version = k.key.version + for network_name in NETWORKS: + net = NETWORKS[network_name] + # not found in this network + if version in [net["xpub"], net["ypub"], net["zpub"]]: + network = net + if version == net["xpub"]: + desc = Descriptor.from_string("pkh(%s)" % str(k)) + elif version == net["ypub"]: + desc = Descriptor.from_string("sh(wpkh(%s))" % str(k)) + elif version == net["zpub"]: + desc = Descriptor.from_string("wpkh(%s)" % str(k)) + break + # we didn't find correct version + if network is None: + raise ValueError("Unknown master public key version") + else: + desc = Descriptor.from_string(masterpub) + if not desc.is_wildcard: + raise ValueError("Descriptor should have wildcards") + for k in desc.keys: + if k.is_extended: + net = detect_network(k) + if net is None: + raise ValueError(f"Unknown version: {k}") + if network is not None and network != net: + raise ValueError("Keys from different networks") + network = net + return desc, network + + +async def create_watch_wallet(*, user: str, masterpub: str, title: str) -> Wallets: + # check the masterpub is fine, it will raise an exception if not + parse_key(masterpub) + wallet_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO watchonly.wallets ( + id, + "user", + masterpub, + title, + address_no, + balance + ) + VALUES (?, ?, ?, ?, ?, ?) + """, + # address_no is -1 so fresh address on empty wallet can get address with index 0 + (wallet_id, user, masterpub, title, -1, 0), + ) + + return await get_watch_wallet(wallet_id) + + +async def get_watch_wallet(wallet_id: str) -> Optional[Wallets]: + row = await db.fetchone( + "SELECT * FROM watchonly.wallets WHERE id = ?", (wallet_id,) + ) + return Wallets.from_row(row) if row else None + + +async def get_watch_wallets(user: str) -> List[Wallets]: + rows = await db.fetchall( + """SELECT * FROM watchonly.wallets WHERE "user" = ?""", (user,) + ) + return [Wallets(**row) for row in rows] + + +async def update_watch_wallet(wallet_id: str, **kwargs) -> Optional[Wallets]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + + await db.execute( + f"UPDATE watchonly.wallets SET {q} WHERE id = ?", (*kwargs.values(), wallet_id) + ) + row = await db.fetchone( + "SELECT * FROM watchonly.wallets WHERE id = ?", (wallet_id,) + ) + return Wallets.from_row(row) if row else None + + +async def delete_watch_wallet(wallet_id: str) -> None: + await db.execute("DELETE FROM watchonly.wallets WHERE id = ?", (wallet_id,)) + + ########################ADDRESSES####################### + + +async def get_derive_address(wallet_id: str, num: int): + wallet = await get_watch_wallet(wallet_id) + key = wallet[2] + desc, network = parse_key(key) + return desc.derive(num).address(network=network) + + +async def get_fresh_address(wallet_id: str) -> Optional[Addresses]: + wallet = await get_watch_wallet(wallet_id) + if not wallet: + return None + + address = await get_derive_address(wallet_id, wallet[4] + 1) + + await update_watch_wallet(wallet_id=wallet_id, address_no=wallet[4] + 1) + masterpub_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO watchonly.addresses ( + id, + address, + wallet, + amount + ) + VALUES (?, ?, ?, ?) + """, + (masterpub_id, address, wallet_id, 0), + ) + + return await get_address(address) + + +async def get_address(address: str) -> Optional[Addresses]: + row = await db.fetchone( + "SELECT * FROM watchonly.addresses WHERE address = ?", (address,) + ) + return Addresses.from_row(row) if row else None + + +async def get_addresses(wallet_id: str) -> List[Addresses]: + rows = await db.fetchall( + "SELECT * FROM watchonly.addresses WHERE wallet = ?", (wallet_id,) + ) + return [Addresses(**row) for row in rows] + + +######################MEMPOOL####################### + + +async def create_mempool(user: str) -> Optional[Mempool]: + await db.execute( + """ + INSERT INTO watchonly.mempool ("user",endpoint) + VALUES (?, ?) + """, + (user, "https://mempool.space"), + ) + row = await db.fetchone( + """SELECT * FROM watchonly.mempool WHERE "user" = ?""", (user,) + ) + return Mempool.from_row(row) if row else None + + +async def update_mempool(user: str, **kwargs) -> Optional[Mempool]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + + await db.execute( + f"""UPDATE watchonly.mempool SET {q} WHERE "user" = ?""", + (*kwargs.values(), user), + ) + row = await db.fetchone( + """SELECT * FROM watchonly.mempool WHERE "user" = ?""", (user,) + ) + return Mempool.from_row(row) if row else None + + +async def get_mempool(user: str) -> Mempool: + row = await db.fetchone( + """SELECT * FROM watchonly.mempool WHERE "user" = ?""", (user,) + ) + return Mempool.from_row(row) if row else None diff --git a/lnbits/extensions/watchonly/migrations.py b/lnbits/extensions/watchonly/migrations.py new file mode 100644 index 00000000..05c229b5 --- /dev/null +++ b/lnbits/extensions/watchonly/migrations.py @@ -0,0 +1,36 @@ +async def m001_initial(db): + """ + Initial wallet table. + """ + await db.execute( + """ + CREATE TABLE watchonly.wallets ( + id TEXT NOT NULL PRIMARY KEY, + "user" TEXT, + masterpub TEXT NOT NULL, + title TEXT NOT NULL, + address_no INTEGER NOT NULL DEFAULT 0, + balance INTEGER NOT NULL + ); + """ + ) + + await db.execute( + """ + CREATE TABLE watchonly.addresses ( + id TEXT NOT NULL PRIMARY KEY, + address TEXT NOT NULL, + wallet TEXT NOT NULL, + amount INTEGER NOT NULL + ); + """ + ) + + await db.execute( + """ + CREATE TABLE watchonly.mempool ( + "user" TEXT NOT NULL, + endpoint TEXT NOT NULL + ); + """ + ) diff --git a/lnbits/extensions/watchonly/models.py b/lnbits/extensions/watchonly/models.py new file mode 100644 index 00000000..b9faa601 --- /dev/null +++ b/lnbits/extensions/watchonly/models.py @@ -0,0 +1,35 @@ +from sqlite3 import Row +from typing import NamedTuple + + +class Wallets(NamedTuple): + id: str + user: str + masterpub: str + title: str + address_no: int + balance: int + + @classmethod + def from_row(cls, row: Row) -> "Wallets": + return cls(**dict(row)) + + +class Mempool(NamedTuple): + user: str + endpoint: str + + @classmethod + def from_row(cls, row: Row) -> "Mempool": + return cls(**dict(row)) + + +class Addresses(NamedTuple): + id: str + address: str + wallet: str + amount: int + + @classmethod + def from_row(cls, row: Row) -> "Addresses": + return cls(**dict(row)) diff --git a/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html b/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html new file mode 100644 index 00000000..97fdb8a9 --- /dev/null +++ b/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html @@ -0,0 +1,244 @@ + + +

+ Watch Only extension uses mempool.space
+ For use with "account Extended Public Key" + https://iancoleman.io/bip39/ + +
Created by, + Ben Arc (using, + Embit
) +

+
+ + + + + + GET /watchonly/api/v1/wallet +
Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<wallets_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/wallet -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /watchonly/api/v1/wallet/<wallet_id> +
Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json) +
+
+ Returns 201 CREATED (application/json) +
+ [<wallet_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/wallet/<wallet_id> + -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + POST /watchonly/api/v1/wallet +
Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json) +
+
+ Returns 201 CREATED (application/json) +
+ [<wallet_object>, ...] +
Curl example
+ curl -X POST {{ request.url_root }}api/v1/wallet -d '{"title": + <string>, "masterpub": <string>}' -H "Content-type: + application/json" -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}" + +
+
+
+ + + + DELETE + /watchonly/api/v1/wallet/<wallet_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Returns 204 NO CONTENT
+ +
Curl example
+ curl -X DELETE {{ request.url_root + }}api/v1/wallet/<wallet_id> -H "X-Api-Key: {{ + g.user.wallets[0].adminkey }}" + +
+
+
+ + + + + GET + /watchonly/api/v1/addresses/<wallet_id> +
Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<address_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root + }}api/v1/addresses/<wallet_id> -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" + +
+
+
+ + + + + GET + /watchonly/api/v1/address/<wallet_id> +
Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<address_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/address/<wallet_id> + -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + + GET /watchonly/api/v1/mempool +
Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<mempool_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/mempool -H "X-Api-Key: {{ + g.user.wallets[0].adminkey }}" + +
+
+
+ + + + + POST + /watchonly/api/v1/mempool +
Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json) +
+
+ Returns 201 CREATED (application/json) +
+ [<mempool_object>, ...] +
Curl example
+ curl -X PUT {{ request.url_root }}api/v1/mempool -d '{"endpoint": + <string>}' -H "Content-type: application/json" -H "X-Api-Key: + {{ g.user.wallets[0].adminkey }}" + +
+
+
+
+
diff --git a/lnbits/extensions/watchonly/templates/watchonly/index.html b/lnbits/extensions/watchonly/templates/watchonly/index.html new file mode 100644 index 00000000..5230e298 --- /dev/null +++ b/lnbits/extensions/watchonly/templates/watchonly/index.html @@ -0,0 +1,649 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + {% raw %} + New wallet + + +
+ Point to another Mempool + {{ this.mempool.endpoint }} + + +
+ set + cancel +
+
+
+
+
+
+ + + +
+
+
Wallets
+
+
+ + + +
+
+ + + + +
+
+ + +
+
{{satBtc(utxos.total)}}
+ + {{utxos.sats ? ' sats' : ' BTC'}} +
+
+ + + +
+
+
Transactions
+
+
+ + + +
+
+ + + + +
+
+
+ + {% endraw %} + +
+ + +
+ {{SITE_TITLE}} Watch Only Extension +
+
+ + + {% include "watchonly/_api_docs.html" %} + +
+
+ + + + + + + + +
+ Create Watch-only Wallet + Cancel +
+
+
+
+ + + + {% raw %} +
Addresses
+
+

+ Current: + {{ currentaddress }} + +

+ + + +

+ + + + {{ data.address }} + + + + +

+ +
+ Get fresh address + Close +
+
+
+ {% endraw %} +
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + + + + +{% endblock %} diff --git a/lnbits/extensions/watchonly/views.py b/lnbits/extensions/watchonly/views.py new file mode 100644 index 00000000..e8246968 --- /dev/null +++ b/lnbits/extensions/watchonly/views.py @@ -0,0 +1,22 @@ +from quart import g, abort, render_template +from http import HTTPStatus + +from lnbits.decorators import check_user_exists, validate_uuids + +from . import watchonly_ext + + +@watchonly_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("watchonly/index.html", user=g.user) + + +@watchonly_ext.route("/") +async def display(charge_id): + link = get_payment(charge_id) or abort( + HTTPStatus.NOT_FOUND, "Charge link does not exist." + ) + + return await render_template("watchonly/display.html", link=link) diff --git a/lnbits/extensions/watchonly/views_api.py b/lnbits/extensions/watchonly/views_api.py new file mode 100644 index 00000000..01ae2527 --- /dev/null +++ b/lnbits/extensions/watchonly/views_api.py @@ -0,0 +1,138 @@ +import hashlib +from quart import g, jsonify, url_for, request +from http import HTTPStatus +import httpx +import json + +from lnbits.core.crud import get_user +from lnbits.decorators import api_check_wallet_key, api_validate_post_request + +from lnbits.extensions.watchonly import watchonly_ext +from .crud import ( + create_watch_wallet, + get_watch_wallet, + get_watch_wallets, + update_watch_wallet, + delete_watch_wallet, + get_fresh_address, + get_addresses, + create_mempool, + update_mempool, + get_mempool, +) + +###################WALLETS############################# + + +@watchonly_ext.route("/api/v1/wallet", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_wallets_retrieve(): + + try: + return ( + jsonify( + [wallet._asdict() for wallet in await get_watch_wallets(g.wallet.user)] + ), + HTTPStatus.OK, + ) + except: + return "" + + +@watchonly_ext.route("/api/v1/wallet/", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_wallet_retrieve(wallet_id): + wallet = await get_watch_wallet(wallet_id) + + if not wallet: + return jsonify({"message": "wallet does not exist"}), HTTPStatus.NOT_FOUND + + return jsonify(wallet._asdict()), HTTPStatus.OK + + +@watchonly_ext.route("/api/v1/wallet", methods=["POST"]) +@api_check_wallet_key("admin") +@api_validate_post_request( + schema={ + "masterpub": {"type": "string", "empty": False, "required": True}, + "title": {"type": "string", "empty": False, "required": True}, + } +) +async def api_wallet_create_or_update(wallet_id=None): + try: + wallet = await create_watch_wallet( + user=g.wallet.user, masterpub=g.data["masterpub"], title=g.data["title"] + ) + except Exception as e: + return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST + mempool = await get_mempool(g.wallet.user) + if not mempool: + create_mempool(user=g.wallet.user) + return jsonify(wallet._asdict()), HTTPStatus.CREATED + + +@watchonly_ext.route("/api/v1/wallet/", methods=["DELETE"]) +@api_check_wallet_key("admin") +async def api_wallet_delete(wallet_id): + wallet = await get_watch_wallet(wallet_id) + + if not wallet: + return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND + + await delete_watch_wallet(wallet_id) + + return jsonify({"deleted": "true"}), HTTPStatus.NO_CONTENT + + +#############################ADDRESSES########################## + + +@watchonly_ext.route("/api/v1/address/", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_fresh_address(wallet_id): + await get_fresh_address(wallet_id) + + addresses = await get_addresses(wallet_id) + + return jsonify([address._asdict() for address in addresses]), HTTPStatus.OK + + +@watchonly_ext.route("/api/v1/addresses/", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_get_addresses(wallet_id): + wallet = await get_watch_wallet(wallet_id) + + if not wallet: + return jsonify({"message": "wallet does not exist"}), HTTPStatus.NOT_FOUND + + addresses = await get_addresses(wallet_id) + + if not addresses: + await get_fresh_address(wallet_id) + addresses = await get_addresses(wallet_id) + + return jsonify([address._asdict() for address in addresses]), HTTPStatus.OK + + +#############################MEMPOOL########################## + + +@watchonly_ext.route("/api/v1/mempool", methods=["PUT"]) +@api_check_wallet_key("admin") +@api_validate_post_request( + schema={ + "endpoint": {"type": "string", "empty": False, "required": True}, + } +) +async def api_update_mempool(): + mempool = await update_mempool(user=g.wallet.user, **g.data) + return jsonify(mempool._asdict()), HTTPStatus.OK + + +@watchonly_ext.route("/api/v1/mempool", methods=["GET"]) +@api_check_wallet_key("admin") +async def api_get_mempool(): + mempool = await get_mempool(g.wallet.user) + if not mempool: + mempool = await create_mempool(user=g.wallet.user) + return jsonify(mempool._asdict()), HTTPStatus.OK From e939666107a36c0a60aab6342bc8a7b2a871aa0d Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Thu, 14 Oct 2021 11:45:56 +0100 Subject: [PATCH 31/75] satspay initial converstion --- lnbits/extensions/satspay/__init__.py | 18 ++- lnbits/extensions/satspay/crud.py | 47 +++---- lnbits/extensions/satspay/models.py | 14 +- .../satspay/templates/satspay/_api_docs.html | 8 +- lnbits/extensions/satspay/views.py | 35 +++-- lnbits/extensions/satspay/views_api.py | 123 ++++++++---------- 6 files changed, 130 insertions(+), 115 deletions(-) diff --git a/lnbits/extensions/satspay/__init__.py b/lnbits/extensions/satspay/__init__.py index 4bdaa2b6..7b7f0bde 100644 --- a/lnbits/extensions/satspay/__init__.py +++ b/lnbits/extensions/satspay/__init__.py @@ -1,13 +1,25 @@ -from quart import Blueprint +import asyncio + +from fastapi import APIRouter + from lnbits.db import Database +from lnbits.helpers import template_renderer db = Database("ext_satspay") -satspay_ext: Blueprint = Blueprint( - "satspay", __name__, static_folder="static", template_folder="templates" +satspay_ext: APIRouter = APIRouter( + prefix="/satspay", + tags=["satspay"] ) +def satspay_renderer(): + return template_renderer( + [ + "lnbits/extensions/satspay/templates", + ] + ) + from .views_api import * # noqa from .views import * # noqa diff --git a/lnbits/extensions/satspay/crud.py b/lnbits/extensions/satspay/crud.py index 56cabdbe..fab0406f 100644 --- a/lnbits/extensions/satspay/crud.py +++ b/lnbits/extensions/satspay/crud.py @@ -2,11 +2,10 @@ from typing import List, Optional, Union # from lnbits.db import open_ext_db from . import db -from .models import Charges +from .models import Charges, CreateCharge from lnbits.helpers import urlsafe_short_hash -from quart import jsonify import httpx from lnbits.core.services import create_invoice, check_invoice_status from ..watchonly.crud import get_watch_wallet, get_fresh_address, get_mempool @@ -17,25 +16,27 @@ from ..watchonly.crud import get_watch_wallet, get_fresh_address, get_mempool async def create_charge( user: str, - description: str = None, - onchainwallet: Optional[str] = None, - lnbitswallet: Optional[str] = None, - webhook: Optional[str] = None, - completelink: Optional[str] = None, - completelinktext: Optional[str] = "Back to Merchant", - time: Optional[int] = None, - amount: Optional[int] = None, + data: CreateCharge + # user: str, + # description: str = None, + # onchainwallet: Optional[str] = None, + # lnbitswallet: Optional[str] = None, + # webhook: Optional[str] = None, + # completelink: Optional[str] = None, + # completelinktext: Optional[str] = "Back to Merchant", + # time: Optional[int] = None, + # amount: Optional[int] = None, ) -> Charges: charge_id = urlsafe_short_hash() - if onchainwallet: - wallet = await get_watch_wallet(onchainwallet) - onchain = await get_fresh_address(onchainwallet) + if data.onchainwallet: + wallet = await get_watch_wallet(data.onchainwallet) + onchain = await get_fresh_address(data.onchainwallet) onchainaddress = onchain.address else: onchainaddress = None - if lnbitswallet: + if data.lnbitswallet: payment_hash, payment_request = await create_invoice( - wallet_id=lnbitswallet, amount=amount, memo=charge_id + wallet_id=data.lnbitswallet, amount=data.amount, memo=charge_id ) else: payment_hash = None @@ -63,17 +64,17 @@ async def create_charge( ( charge_id, user, - description, - onchainwallet, + data.description, + data.onchainwallet, onchainaddress, - lnbitswallet, + data.lnbitswallet, payment_request, payment_hash, - webhook, - completelink, - completelinktext, - time, - amount, + data.webhook, + data.completelink, + data.completelinktext, + data.time, + data.amount, 0, ), ) diff --git a/lnbits/extensions/satspay/models.py b/lnbits/extensions/satspay/models.py index a7bfa14f..8730809b 100644 --- a/lnbits/extensions/satspay/models.py +++ b/lnbits/extensions/satspay/models.py @@ -1,9 +1,19 @@ from sqlite3 import Row -from typing import NamedTuple +from fastapi.param_functions import Query +from pydantic import BaseModel import time +class CreateCharge(BaseModel): + onchainwallet: str = Query(None) + lnbitswallet: str = Query(None) + description: str = Query(...) + webhook: str = Query(None) + completelink: str = Query(None) + completelinktext: str = Query(None) + time: int = Query(..., ge=1) + amount: int = Query(..., ge=1) -class Charges(NamedTuple): +class Charges(BaseModel): id: str user: str description: str diff --git a/lnbits/extensions/satspay/templates/satspay/_api_docs.html b/lnbits/extensions/satspay/templates/satspay/_api_docs.html index 526af7f3..1a7ba5e4 100644 --- a/lnbits/extensions/satspay/templates/satspay/_api_docs.html +++ b/lnbits/extensions/satspay/templates/satspay/_api_docs.html @@ -90,7 +90,7 @@
Curl example
curl -X GET {{ request.url_root }}api/v1/charge/<charge_id> - -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" + -H "X-Api-Key: {{ user.wallets[0].inkey }}" @@ -113,7 +113,7 @@
Curl example
curl -X GET {{ request.url_root }}api/v1/charges -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" + user.wallets[0].inkey }}" @@ -139,7 +139,7 @@ curl -X DELETE {{ request.url_root }}api/v1/charge/<charge_id> -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" + user.wallets[0].adminkey }}" @@ -162,7 +162,7 @@ curl -X GET {{ request.url_root }}api/v1/charges/balance/<charge_id> -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" + user.wallets[0].inkey }}" diff --git a/lnbits/extensions/satspay/views.py b/lnbits/extensions/satspay/views.py index 2c99a925..c1be381c 100644 --- a/lnbits/extensions/satspay/views.py +++ b/lnbits/extensions/satspay/views.py @@ -1,22 +1,29 @@ -from quart import g, abort, render_template, jsonify +from fastapi.param_functions import Depends +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse +from lnbits.core.models import User +from lnbits.core.crud import get_wallet +from lnbits.decorators import check_user_exists from http import HTTPStatus -from lnbits.decorators import check_user_exists, validate_uuids +from fastapi.templating import Jinja2Templates -from . import satspay_ext +from . import satspay_ext, satspay_renderer from .crud import get_charge +templates = Jinja2Templates(directory="templates") -@satspay_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(): - return await render_template("satspay/index.html", user=g.user) +@satspay_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return satspay_renderer().TemplateResponse("satspay/index.html", {"request": request,"user": user.dict()}) -@satspay_ext.route("/") -async def display(charge_id): - charge = await get_charge(charge_id) or abort( - HTTPStatus.NOT_FOUND, "Charge link does not exist." - ) - return await render_template("satspay/display.html", charge=charge) +@satspay_ext.get("/{charge_id}", response_class=HTMLResponse) +async def display(request: Request, charge_id): + charge = await get_charge(charge_id) + if not charge: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Charge link does not exist." + ) + return satspay_renderer().TemplateResponse("satspay/display.html", {"request": request, "charge": charge}) diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py index 9440312a..85590e5e 100644 --- a/lnbits/extensions/satspay/views_api.py +++ b/lnbits/extensions/satspay/views_api.py @@ -1,13 +1,21 @@ import hashlib -from quart import g, jsonify, url_for + from http import HTTPStatus import httpx +from fastapi import Query +from fastapi.params import Depends + +from starlette.exceptions import HTTPException +from starlette.requests import Request +from starlette.responses import HTMLResponse, JSONResponse # type: ignore + from lnbits.core.crud import get_user -from lnbits.decorators import api_check_wallet_key, api_validate_post_request +from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.extensions.satspay import satspay_ext +from .models import CreateCharge from .crud import ( create_charge, update_charge, @@ -20,94 +28,78 @@ from .crud import ( #############################CHARGES########################## -@satspay_ext.route("/api/v1/charge", methods=["POST"]) -@satspay_ext.route("/api/v1/charge/", methods=["PUT"]) -@api_check_wallet_key("admin") -@api_validate_post_request( - schema={ - "onchainwallet": {"type": "string"}, - "lnbitswallet": {"type": "string"}, - "description": {"type": "string", "empty": False, "required": True}, - "webhook": {"type": "string"}, - "completelink": {"type": "string"}, - "completelinktext": {"type": "string"}, - "time": {"type": "integer", "min": 1, "required": True}, - "amount": {"type": "integer", "min": 1, "required": True}, - } -) -async def api_charge_create_or_update(charge_id=None): +@satspay_ext.post("/api/v1/charge") +@satspay_ext.put("/api/v1/charge/{charge_id}") + +async def api_charge_create_or_update(data: CreateCharge, wallet: WalletTypeInfo = Depends(get_key_type), charge_id=None): if not charge_id: - charge = await create_charge(user=g.wallet.user, **g.data) - return jsonify(charge._asdict()), HTTPStatus.CREATED + charge = await create_charge(user=wallet.wallet.user, **data) + return charge.dict() else: - charge = await update_charge(charge_id=charge_id, **g.data) - return jsonify(charge._asdict()), HTTPStatus.OK + charge = await update_charge(charge_id=charge_id, **data) + return charge.dict() -@satspay_ext.route("/api/v1/charges", methods=["GET"]) -@api_check_wallet_key("invoice") -async def api_charges_retrieve(): +@satspay_ext.get("/api/v1/charges") +async def api_charges_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)): try: - return ( - jsonify( - [ + return [ { - **charge._asdict(), + **charge.dict(), **{"time_elapsed": charge.time_elapsed}, **{"paid": charge.paid}, } - for charge in await get_charges(g.wallet.user) + for charge in await get_charges(wallet.wallet.user) ] - ), - HTTPStatus.OK, - ) except: return "" -@satspay_ext.route("/api/v1/charge/", methods=["GET"]) -@api_check_wallet_key("invoice") -async def api_charge_retrieve(charge_id): +@satspay_ext.get("/api/v1/charge/{charge_id}") +async def api_charge_retrieve(charge_id, wallet: WalletTypeInfo = Depends(get_key_type)): charge = await get_charge(charge_id) if not charge: - return jsonify({"message": "charge does not exist"}), HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Charge does not exist." + ) - return ( - jsonify( - { - **charge._asdict(), + return { + **charge.dict(), **{"time_elapsed": charge.time_elapsed}, **{"paid": charge.paid}, } - ), - HTTPStatus.OK, - ) -@satspay_ext.route("/api/v1/charge/", methods=["DELETE"]) -@api_check_wallet_key("invoice") -async def api_charge_delete(charge_id): +@satspay_ext.delete("/api/v1/charge/{charge_id}") +async def api_charge_delete(charge_id, wallet: WalletTypeInfo = Depends(get_key_type)): charge = await get_charge(charge_id) if not charge: - return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Charge does not exist." + ) await delete_charge(charge_id) - - return "", HTTPStatus.NO_CONTENT + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) #############################BALANCE########################## -@satspay_ext.route("/api/v1/charges/balance/", methods=["GET"]) +@satspay_ext.get("/api/v1/charges/balance/{charge_id}") async def api_charges_balance(charge_id): charge = await check_address_balance(charge_id) if not charge: - return jsonify({"message": "charge does not exist"}), HTTPStatus.NOT_FOUND + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Charge does not exist." + ) + if charge.paid and charge.webhook: async with httpx.AsyncClient() as client: try: @@ -130,28 +122,21 @@ async def api_charges_balance(charge_id): ) except AssertionError: charge.webhook = None - return jsonify(charge._asdict()), HTTPStatus.OK + return charge.dict() #############################MEMPOOL########################## -@satspay_ext.route("/api/v1/mempool", methods=["PUT"]) -@api_check_wallet_key("invoice") -@api_validate_post_request( - schema={ - "endpoint": {"type": "string", "empty": False, "required": True}, - } -) -async def api_update_mempool(): - mempool = await update_mempool(user=g.wallet.user, **g.data) - return jsonify(mempool._asdict()), HTTPStatus.OK +@satspay_ext.put("/api/v1/mempool") +async def api_update_mempool(endpoint: str = Query(...), wallet: WalletTypeInfo = Depends(get_key_type)): + mempool = await update_mempool(endpoint, user=wallet.wallet.user) + return mempool.dict() -@satspay_ext.route("/api/v1/mempool", methods=["GET"]) -@api_check_wallet_key("invoice") -async def api_get_mempool(): - mempool = await get_mempool(g.wallet.user) +@satspay_ext.route("/api/v1/mempool") +async def api_get_mempool(wallet: WalletTypeInfo = Depends(get_key_type)): + mempool = await get_mempool(wallet.wallet.user) if not mempool: - mempool = await create_mempool(user=g.wallet.user) - return jsonify(mempool._asdict()), HTTPStatus.OK + mempool = await create_mempool(user=wallet.wallet.user) + return mempool.dict() From ec4117a5f45739443d7630f67a1141dd3c1d4779 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Thu, 14 Oct 2021 22:30:29 +0100 Subject: [PATCH 32/75] satspay done --- lnbits/extensions/satspay/crud.py | 9 --------- lnbits/extensions/satspay/models.py | 15 ++++++++------- .../satspay/templates/satspay/_api_docs.html | 4 ++-- .../satspay/templates/satspay/display.html | 3 ++- .../satspay/templates/satspay/index.html | 4 +++- lnbits/extensions/satspay/views.py | 1 + lnbits/extensions/satspay/views_api.py | 4 ++-- 7 files changed, 18 insertions(+), 22 deletions(-) diff --git a/lnbits/extensions/satspay/crud.py b/lnbits/extensions/satspay/crud.py index fab0406f..e707dc00 100644 --- a/lnbits/extensions/satspay/crud.py +++ b/lnbits/extensions/satspay/crud.py @@ -17,15 +17,6 @@ from ..watchonly.crud import get_watch_wallet, get_fresh_address, get_mempool async def create_charge( user: str, data: CreateCharge - # user: str, - # description: str = None, - # onchainwallet: Optional[str] = None, - # lnbitswallet: Optional[str] = None, - # webhook: Optional[str] = None, - # completelink: Optional[str] = None, - # completelinktext: Optional[str] = "Back to Merchant", - # time: Optional[int] = None, - # amount: Optional[int] = None, ) -> Charges: charge_id = urlsafe_short_hash() if data.onchainwallet: diff --git a/lnbits/extensions/satspay/models.py b/lnbits/extensions/satspay/models.py index 8730809b..4cf3efad 100644 --- a/lnbits/extensions/satspay/models.py +++ b/lnbits/extensions/satspay/models.py @@ -1,4 +1,5 @@ from sqlite3 import Row +from typing import Optional from fastapi.param_functions import Query from pydantic import BaseModel import time @@ -16,15 +17,15 @@ class CreateCharge(BaseModel): class Charges(BaseModel): id: str user: str - description: str - onchainwallet: str - onchainaddress: str - lnbitswallet: str + description: Optional[str] + onchainwallet: Optional[str] + onchainaddress: Optional[str] + lnbitswallet: Optional[str] payment_request: str payment_hash: str - webhook: str - completelink: str - completelinktext: str + webhook: Optional[str] + completelink: Optional[str] + completelinktext: Optional[str] = "Back to Merchant" time: int amount: int balance: int diff --git a/lnbits/extensions/satspay/templates/satspay/_api_docs.html b/lnbits/extensions/satspay/templates/satspay/_api_docs.html index 1a7ba5e4..af95cbf2 100644 --- a/lnbits/extensions/satspay/templates/satspay/_api_docs.html +++ b/lnbits/extensions/satspay/templates/satspay/_api_docs.html @@ -37,7 +37,7 @@ "description": <string>, "webhook":<string>, "time": <integer>, "amount": <integer>, "lnbitswallet": <string, lnbits_wallet_id>}' -H "Content-type: - application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}" + application/json" -H "X-Api-Key: {{user.wallets[0].adminkey }}" @@ -65,7 +65,7 @@ "description": <string>, "webhook":<string>, "time": <integer>, "amount": <integer>, "lnbitswallet": <string, lnbits_wallet_id>}' -H "Content-type: - application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}" + application/json" -H "X-Api-Key: {{user.wallets[0].adminkey }}" diff --git a/lnbits/extensions/satspay/templates/satspay/display.html b/lnbits/extensions/satspay/templates/satspay/display.html index b3386074..5b0282b6 100644 --- a/lnbits/extensions/satspay/templates/satspay/display.html +++ b/lnbits/extensions/satspay/templates/satspay/display.html @@ -207,7 +207,7 @@ {% endblock %} {% block scripts %} - + +{% endblock %} diff --git a/lnbits/extensions/splitpayments/views.py b/lnbits/extensions/splitpayments/views.py new file mode 100644 index 00000000..78e40736 --- /dev/null +++ b/lnbits/extensions/splitpayments/views.py @@ -0,0 +1,18 @@ +from http import HTTPStatus +from lnbits.decorators import check_user_exists, WalletTypeInfo, get_key_type +from . import splitpayments_ext, splitpayments_renderer +from fastapi import FastAPI, Request +from fastapi.params import Depends +from fastapi.templating import Jinja2Templates +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse +from lnbits.core.models import User, Payment + +templates = Jinja2Templates(directory="templates") + + +@splitpayments_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return splitpayments_renderer().TemplateResponse( + "splitpayments/index.html", {"request": request, "user": user.dict()} + ) diff --git a/lnbits/extensions/splitpayments/views_api.py b/lnbits/extensions/splitpayments/views_api.py new file mode 100644 index 00000000..33ed266a --- /dev/null +++ b/lnbits/extensions/splitpayments/views_api.py @@ -0,0 +1,70 @@ +import json +import httpx +import base64 +from .crud import get_targets, set_targets +from .models import Target, TargetPut +from fastapi import Request +from http import HTTPStatus +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse, JSONResponse # type: ignore +from typing import Optional +from fastapi.params import Depends +from fastapi.param_functions import Query +from . import splitpayments_ext +from lnbits.decorators import ( + check_user_exists, + WalletTypeInfo, + get_key_type, + api_validate_post_request, + WalletAdminKeyChecker, + WalletInvoiceKeyChecker, +) +from lnbits.core.crud import get_wallet, get_wallet_for_key + + +@splitpayments_ext.get("/api/v1/targets") +async def api_targets_get(wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker())): + targets = await get_targets(wallet.wallet.id) + return [target.dict() for target in targets] or [] + + +@splitpayments_ext.put("/api/v1/targets") +async def api_targets_set( + data: TargetPut, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker()) +): + targets = [] + for entry in data["targets"]: + wallet = await get_wallet(entry["wallet"]) + if not wallet: + wallet = await get_wallet_for_key(entry["wallet"], "invoice") + if not wallet: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"Invalid wallet '{entry['wallet']}'.", + ) + + if wallet.id == wallet.wallet.id: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="Can't split to itself.", + ) + + if entry["percent"] < 0: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"Invalid percent '{entry['percent']}'.", + ) + + targets.append( + Target(wallet.id, wallet.wallet.id, entry["percent"], entry["alias"] or "") + ) + + percent_sum = sum([target.percent for target in targets]) + if percent_sum > 100: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="Splitting over 100%.", + ) + + await set_targets(wallet.wallet.id, targets) + return "" From 3285e9d3c95076e81e96bd0f4aaca25f94eead4c Mon Sep 17 00:00:00 2001 From: benarc Date: Mon, 18 Oct 2021 13:02:28 +0100 Subject: [PATCH 58/75] Splitpayments booting, but not sure how to handle internal pays --- lnbits/extensions/splitpayments/__init__.py | 6 +++--- lnbits/extensions/splitpayments/tasks.py | 5 +++-- lnbits/extensions/splitpayments/views_api.py | 14 +++++++------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lnbits/extensions/splitpayments/__init__.py b/lnbits/extensions/splitpayments/__init__.py index 33062425..b1e0fbdf 100644 --- a/lnbits/extensions/splitpayments/__init__.py +++ b/lnbits/extensions/splitpayments/__init__.py @@ -8,11 +8,11 @@ from lnbits.tasks import catch_everything_and_restart db = Database("ext_splitpayments") -copilot_static_files = [ +splitpayments_static_files = [ { - "path": "/copilot/static", + "path": "/splitpayments/static", "app": StaticFiles(directory="lnbits/extensions/splitpayments/static"), - "name": "copilot_static", + "name": "splitpayments_static", } ] splitpayments_ext: APIRouter = APIRouter( diff --git a/lnbits/extensions/splitpayments/tasks.py b/lnbits/extensions/splitpayments/tasks.py index 2f8d862f..1a1b83aa 100644 --- a/lnbits/extensions/splitpayments/tasks.py +++ b/lnbits/extensions/splitpayments/tasks.py @@ -3,7 +3,7 @@ import json from lnbits.core.models import Payment from lnbits.core.crud import create_payment from lnbits.core import db as core_db -from lnbits.tasks import register_invoice_listener, internal_invoice_paid +from lnbits.tasks import register_invoice_listener # , internal_invoice_paid from lnbits.helpers import urlsafe_short_hash from .crud import get_targets @@ -78,4 +78,5 @@ async def on_invoice_paid(payment: Payment) -> None: ) # manually send this for now - await internal_invoice_paid.send(internal_checking_id) + # await internal_invoice_paid.send(internal_checking_id) + return diff --git a/lnbits/extensions/splitpayments/views_api.py b/lnbits/extensions/splitpayments/views_api.py index 33ed266a..7e43d3a7 100644 --- a/lnbits/extensions/splitpayments/views_api.py +++ b/lnbits/extensions/splitpayments/views_api.py @@ -33,14 +33,14 @@ async def api_targets_set( data: TargetPut, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker()) ): targets = [] - for entry in data["targets"]: - wallet = await get_wallet(entry["wallet"]) + for entry in data.targets: + wallet = await get_wallet(entry.wallet) if not wallet: - wallet = await get_wallet_for_key(entry["wallet"], "invoice") + wallet = await get_wallet_for_key(entry.wallet, "invoice") if not wallet: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, - detail=f"Invalid wallet '{entry['wallet']}'.", + detail=f"Invalid wallet '{entry.wallet}'.", ) if wallet.id == wallet.wallet.id: @@ -49,14 +49,14 @@ async def api_targets_set( detail="Can't split to itself.", ) - if entry["percent"] < 0: + if entry.percent < 0: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, - detail=f"Invalid percent '{entry['percent']}'.", + detail=f"Invalid percent '{entry.percent}'.", ) targets.append( - Target(wallet.id, wallet.wallet.id, entry["percent"], entry["alias"] or "") + Target(wallet.id, wallet.wallet.id, entry.percent, entry.alias or "") ) percent_sum = sum([target.percent for target in targets]) From cf6fae2ca7ef8af74b9f262f44e7fbf18eaf90af Mon Sep 17 00:00:00 2001 From: benarc Date: Mon, 18 Oct 2021 13:24:32 +0100 Subject: [PATCH 59/75] Added internal payment listener to paid invoices and splitpayments --- lnbits/core/services.py | 7 +++---- lnbits/extensions/splitpayments/tasks.py | 4 ++-- .../splitpayments/templates/splitpayments/_api_docs.html | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lnbits/core/services.py b/lnbits/core/services.py index c48b3296..a7577217 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -32,8 +32,6 @@ except ImportError: # pragma: nocover from typing_extensions import TypedDict - - class PaymentFailure(Exception): pass @@ -155,9 +153,10 @@ async def pay_invoice( ) # notify receiver asynchronously - from lnbits.tasks import internal_invoice_paid - await internal_invoice_paid.send(internal_checking_id) + from lnbits.tasks import internal_invoice_queue + + await internal_invoice_queue.put(internal_checking_id) else: # actually pay the external invoice payment: PaymentResponse = await WALLET.pay_invoice(payment_request) diff --git a/lnbits/extensions/splitpayments/tasks.py b/lnbits/extensions/splitpayments/tasks.py index 1a1b83aa..12612782 100644 --- a/lnbits/extensions/splitpayments/tasks.py +++ b/lnbits/extensions/splitpayments/tasks.py @@ -3,7 +3,7 @@ import json from lnbits.core.models import Payment from lnbits.core.crud import create_payment from lnbits.core import db as core_db -from lnbits.tasks import register_invoice_listener # , internal_invoice_paid +from lnbits.tasks import register_invoice_listener, internal_invoice_queue from lnbits.helpers import urlsafe_short_hash from .crud import get_targets @@ -78,5 +78,5 @@ async def on_invoice_paid(payment: Payment) -> None: ) # manually send this for now - # await internal_invoice_paid.send(internal_checking_id) + await internal_invoice_queue.put(internal_checking_id) return diff --git a/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html b/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html index e92fac96..116bdd74 100644 --- a/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html +++ b/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html @@ -53,7 +53,7 @@
Curl example
curl -X GET {{ request.url_root }}api/v1/livestream -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" + user.wallets[0].inkey }}" @@ -79,7 +79,7 @@
Curl example
curl -X PUT {{ request.url_root }}api/v1/splitpayments/targets -H - "X-Api-Key: {{ g.user.wallets[0].adminkey }}" -H 'Content-Type: + "X-Api-Key: {{ user.wallets[0].adminkey }}" -H 'Content-Type: application/json' -d '{"targets": [{"wallet": <wallet id or invoice key>, "alias": <name to identify this>, "percent": <number between 1 and 100>}, ...]}' From cfd37ec31e0ac756f2d344afc093dfd2c50b1a09 Mon Sep 17 00:00:00 2001 From: benarc Date: Mon, 18 Oct 2021 13:35:31 +0100 Subject: [PATCH 60/75] splitpayments auth issues using WalletAdminKeyChecker --- lnbits/extensions/splitpayments/views_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lnbits/extensions/splitpayments/views_api.py b/lnbits/extensions/splitpayments/views_api.py index 7e43d3a7..034daf9b 100644 --- a/lnbits/extensions/splitpayments/views_api.py +++ b/lnbits/extensions/splitpayments/views_api.py @@ -24,6 +24,7 @@ from lnbits.core.crud import get_wallet, get_wallet_for_key @splitpayments_ext.get("/api/v1/targets") async def api_targets_get(wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker())): + print(wallet) targets = await get_targets(wallet.wallet.id) return [target.dict() for target in targets] or [] From 4739a0811dc656af55663d7badf9706d06f03b35 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Mon, 18 Oct 2021 16:06:06 +0100 Subject: [PATCH 61/75] added admin key required --- lnbits/decorators.py | 91 +++++++++----------------------------------- 1 file changed, 18 insertions(+), 73 deletions(-) diff --git a/lnbits/decorators.py b/lnbits/decorators.py index 74dc42b3..04f0e220 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -1,24 +1,17 @@ -from functools import wraps from http import HTTPStatus -from base64 import b64decode - -from fastapi.security import api_key -from pydantic.types import UUID4 -from lnbits.core.models import User, Wallet -from typing import List, Union -from uuid import UUID from cerberus import Validator # type: ignore +from fastapi import status from fastapi.exceptions import HTTPException from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.params import Security from fastapi.security.api_key import APIKeyHeader, APIKeyQuery -from fastapi.security import OAuth2PasswordBearer from fastapi.security.base import SecurityBase -from fastapi import status +from pydantic.types import UUID4 from starlette.requests import Request from lnbits.core.crud import get_user, get_wallet_for_key +from lnbits.core.models import User, Wallet from lnbits.requestvars import g from lnbits.settings import LNBITS_ALLOWED_USERS @@ -160,71 +153,23 @@ async def get_key_type( raise -# api_key_header = APIKeyHeader(name="X-API-KEY", auto_error=False, description="Admin or Invoice key for wallet API's") -# api_key_query = APIKeyQuery(name="api-key", auto_error=False, description="Admin or Invoice key for wallet API's") -# oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") -# async def get_key_type(r: Request, -# token: str = Security(oauth2_scheme), -# api_key_header: str = Security(api_key_header), -# api_key_query: str = Security(api_key_query)) -> WalletTypeInfo: -# # 0: admin -# # 1: invoice -# # 2: invalid -# # print("TOKEN", b64decode(token).decode("utf-8").split(":")) -# -# key_type, key = b64decode(token).decode("utf-8").split(":") -# try: -# checker = WalletAdminKeyChecker(api_key=key if token else api_key_query) -# await checker.__call__(r) -# return WalletTypeInfo(0, checker.wallet) -# except HTTPException as e: -# if e.status_code == HTTPStatus.BAD_REQUEST: -# raise -# if e.status_code == HTTPStatus.UNAUTHORIZED: -# pass -# except: -# raise -# -# try: -# checker = WalletInvoiceKeyChecker(api_key=key if token else None) -# await checker.__call__(r) -# return WalletTypeInfo(1, checker.wallet) -# except HTTPException as e: -# if e.status_code == HTTPStatus.BAD_REQUEST: -# raise -# if e.status_code == HTTPStatus.UNAUTHORIZED: -# return WalletTypeInfo(2, None) -# except: -# raise +async def require_admin_key( + r: Request, + api_key_header: str = Security(api_key_header), + api_key_query: str = Security(api_key_query), +): + token = api_key_header if api_key_header else api_key_query + wallet = await get_key_type(r, token) -def api_validate_post_request(*, schema: dict): - def wrap(view): - @wraps(view) - async def wrapped_view(**kwargs): - if "application/json" not in request.headers["Content-Type"]: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail=jsonify( - {"message": "Content-Type must be `application/json`."} - ), - ) - - v = Validator(schema) - data = await request.get_json() - g().data = {key: data[key] for key in schema.keys() if key in data} - - if not v.validate(g().data): - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail=jsonify({"message": f"Errors in request data: {v.errors}"}), - ) - - return await view(**kwargs) - - return wrapped_view - - return wrap + if wallet.wallet_type != 0: + # If wallet type is not admin then return the unauthorized status + # This also covers when the user passes an invalid key type + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Admin key required." + ) + else: + return wallet async def check_user_exists(usr: UUID4) -> User: From cf23e56dc549ab974906de2a8d4d22480587d55a Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Mon, 18 Oct 2021 16:23:51 +0100 Subject: [PATCH 62/75] cleanup/fix breaking imports --- lnbits/extensions/jukebox/views.py | 18 +++++----- lnbits/extensions/jukebox/views_api.py | 36 +++++++++---------- lnbits/extensions/satsdice/views_api.py | 37 ++++++++------------ lnbits/extensions/splitpayments/views_api.py | 34 ++++++++---------- 4 files changed, 54 insertions(+), 71 deletions(-) diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py index 230a61e3..dec65eb9 100644 --- a/lnbits/extensions/jukebox/views.py +++ b/lnbits/extensions/jukebox/views.py @@ -1,19 +1,17 @@ -import json -import time - -from datetime import datetime from http import HTTPStatus -from lnbits.decorators import check_user_exists, WalletTypeInfo, get_key_type -from . import jukebox_ext, jukebox_renderer -from .crud import get_jukebox -from fastapi import FastAPI, Request + +from fastapi import Request from fastapi.params import Depends from fastapi.templating import Jinja2Templates from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse -from lnbits.core.models import User, Payment -from .views_api import api_get_jukebox_device_check +from lnbits.core.models import User +from lnbits.decorators import WalletTypeInfo, check_user_exists, get_key_type + +from . import jukebox_ext, jukebox_renderer +from .crud import get_jukebox +from .views_api import api_get_jukebox_device_check templates = Jinja2Templates(directory="templates") diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 3065d4f6..e6403d00 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -1,34 +1,30 @@ -from fastapi import Request - +import base64 +import json from http import HTTPStatus + +import httpx +from fastapi import Request +from fastapi.param_functions import Query +from fastapi.params import Depends from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse, JSONResponse # type: ignore -import base64 + from lnbits.core.crud import get_wallet -from lnbits.core.services import create_invoice, check_invoice_status -import json -from typing import Optional -from fastapi.params import Depends -from fastapi.param_functions import Query -from .models import CreateJukeLinkData, CreateJukeboxPayment -from lnbits.decorators import ( - check_user_exists, - WalletTypeInfo, - get_key_type, - api_validate_post_request, -) -import httpx +from lnbits.core.services import check_invoice_status, create_invoice +from lnbits.decorators import WalletTypeInfo, get_key_type + from . import jukebox_ext from .crud import ( create_jukebox, - update_jukebox, - get_jukebox, - get_jukeboxs, - delete_jukebox, create_jukebox_payment, + delete_jukebox, + get_jukebox, get_jukebox_payment, + get_jukeboxs, + update_jukebox, update_jukebox_payment, ) +from .models import CreateJukeboxPayment, CreateJukeLinkData @jukebox_ext.get("/api/v1/jukebox") diff --git a/lnbits/extensions/satsdice/views_api.py b/lnbits/extensions/satsdice/views_api.py index 8c5f9d0e..3c7471de 100644 --- a/lnbits/extensions/satsdice/views_api.py +++ b/lnbits/extensions/satsdice/views_api.py @@ -1,35 +1,28 @@ from http import HTTPStatus -from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore -from http import HTTPStatus -from starlette.exceptions import HTTPException -from starlette.responses import HTMLResponse, JSONResponse # type: ignore -from lnbits.core.crud import get_user -from lnbits.decorators import api_validate_post_request -from .models import CreateSatsDiceLink, CreateSatsDiceWithdraws, CreateSatsDicePayment -from . import satsdice_ext -from fastapi import FastAPI, Request -from fastapi.params import Depends -from typing import Optional + +from fastapi import Request from fastapi.param_functions import Query +from fastapi.params import Depends +from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore +from starlette.exceptions import HTTPException + +from lnbits.core.crud import get_user +from lnbits.decorators import WalletTypeInfo, get_key_type + +from . import satsdice_ext from .crud import ( create_satsdice_pay, + create_satsdice_withdraw, + delete_satsdice_pay, + delete_satsdice_withdraw, get_satsdice_pay, get_satsdice_pays, - update_satsdice_pay, - delete_satsdice_pay, - create_satsdice_withdraw, get_satsdice_withdraw, get_satsdice_withdraws, + update_satsdice_pay, update_satsdice_withdraw, - delete_satsdice_withdraw, - create_withdraw_hash_check, -) -from lnbits.decorators import ( - check_user_exists, - WalletTypeInfo, - get_key_type, - api_validate_post_request, ) +from .models import CreateSatsDiceLink, CreateSatsDiceWithdraws ################LNURL pay diff --git a/lnbits/extensions/splitpayments/views_api.py b/lnbits/extensions/splitpayments/views_api.py index 034daf9b..44e1e4b2 100644 --- a/lnbits/extensions/splitpayments/views_api.py +++ b/lnbits/extensions/splitpayments/views_api.py @@ -1,29 +1,25 @@ -import json -import httpx import base64 -from .crud import get_targets, set_targets -from .models import Target, TargetPut -from fastapi import Request +import json from http import HTTPStatus +from typing import Optional + +import httpx +from fastapi import Request +from fastapi.param_functions import Query +from fastapi.params import Depends from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse, JSONResponse # type: ignore -from typing import Optional -from fastapi.params import Depends -from fastapi.param_functions import Query -from . import splitpayments_ext -from lnbits.decorators import ( - check_user_exists, - WalletTypeInfo, - get_key_type, - api_validate_post_request, - WalletAdminKeyChecker, - WalletInvoiceKeyChecker, -) + from lnbits.core.crud import get_wallet, get_wallet_for_key +from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key + +from . import splitpayments_ext +from .crud import get_targets, set_targets +from .models import Target, TargetPut @splitpayments_ext.get("/api/v1/targets") -async def api_targets_get(wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker())): +async def api_targets_get(wallet: WalletTypeInfo = Depends(require_admin_key)): print(wallet) targets = await get_targets(wallet.wallet.id) return [target.dict() for target in targets] or [] @@ -31,7 +27,7 @@ async def api_targets_get(wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker @splitpayments_ext.put("/api/v1/targets") async def api_targets_set( - data: TargetPut, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker()) + data: TargetPut, wallet: WalletTypeInfo = Depends(require_admin_key) ): targets = [] for entry in data.targets: From ca1ad33c7b7c49bec601e4c63cc7f2940dada822 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Mon, 18 Oct 2021 19:39:35 +0100 Subject: [PATCH 63/75] splitpayments working - hard one --- lnbits/extensions/splitpayments/models.py | 17 +++++++------- .../splitpayments/static/js/index.js | 2 +- lnbits/extensions/splitpayments/views.py | 12 +++++----- lnbits/extensions/splitpayments/views_api.py | 22 ++++++++----------- 4 files changed, 24 insertions(+), 29 deletions(-) diff --git a/lnbits/extensions/splitpayments/models.py b/lnbits/extensions/splitpayments/models.py index 14165981..1c38b37d 100644 --- a/lnbits/extensions/splitpayments/models.py +++ b/lnbits/extensions/splitpayments/models.py @@ -1,21 +1,20 @@ -from pydantic.main import BaseModel +from typing import List, Optional + +from fastapi.param_functions import Query from pydantic import BaseModel -from fastapi import FastAPI, Request -from typing import List class Target(BaseModel): wallet: str source: str percent: int - alias: str - + alias: Optional[str] class TargetPutList(BaseModel): - wallet: str - aliat: str - percent: int + wallet: str = Query(...) + alias: str = Query("") + percent: int = Query(..., ge=1) class TargetPut(BaseModel): - targets: List[TargetPutList] + __root__: List[TargetPutList] diff --git a/lnbits/extensions/splitpayments/static/js/index.js b/lnbits/extensions/splitpayments/static/js/index.js index d9750bef..dea469e5 100644 --- a/lnbits/extensions/splitpayments/static/js/index.js +++ b/lnbits/extensions/splitpayments/static/js/index.js @@ -119,7 +119,7 @@ new Vue({ '/splitpayments/api/v1/targets', this.selectedWallet.adminkey, { - targets: this.targets + "targets": this.targets .filter(isTargetComplete) .map(({wallet, percent, alias}) => ({wallet, percent, alias})) } diff --git a/lnbits/extensions/splitpayments/views.py b/lnbits/extensions/splitpayments/views.py index 78e40736..056c7563 100644 --- a/lnbits/extensions/splitpayments/views.py +++ b/lnbits/extensions/splitpayments/views.py @@ -1,12 +1,12 @@ -from http import HTTPStatus -from lnbits.decorators import check_user_exists, WalletTypeInfo, get_key_type -from . import splitpayments_ext, splitpayments_renderer -from fastapi import FastAPI, Request +from fastapi import Request from fastapi.params import Depends from fastapi.templating import Jinja2Templates -from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse -from lnbits.core.models import User, Payment + +from lnbits.core.models import User +from lnbits.decorators import check_user_exists + +from . import splitpayments_ext, splitpayments_renderer templates = Jinja2Templates(directory="templates") diff --git a/lnbits/extensions/splitpayments/views_api.py b/lnbits/extensions/splitpayments/views_api.py index 44e1e4b2..7b532218 100644 --- a/lnbits/extensions/splitpayments/views_api.py +++ b/lnbits/extensions/splitpayments/views_api.py @@ -1,17 +1,11 @@ -import base64 -import json from http import HTTPStatus -from typing import Optional -import httpx from fastapi import Request -from fastapi.param_functions import Query from fastapi.params import Depends from starlette.exceptions import HTTPException -from starlette.responses import HTMLResponse, JSONResponse # type: ignore from lnbits.core.crud import get_wallet, get_wallet_for_key -from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key +from lnbits.decorators import WalletTypeInfo, require_admin_key from . import splitpayments_ext from .crud import get_targets, set_targets @@ -20,17 +14,19 @@ from .models import Target, TargetPut @splitpayments_ext.get("/api/v1/targets") async def api_targets_get(wallet: WalletTypeInfo = Depends(require_admin_key)): - print(wallet) targets = await get_targets(wallet.wallet.id) return [target.dict() for target in targets] or [] @splitpayments_ext.put("/api/v1/targets") async def api_targets_set( - data: TargetPut, wallet: WalletTypeInfo = Depends(require_admin_key) + req: Request, wal: WalletTypeInfo = Depends(require_admin_key) ): + body = await req.json() targets = [] - for entry in data.targets: + data = TargetPut.parse_obj(body["targets"]) + for entry in data.__root__: + print("ENTRY", entry) wallet = await get_wallet(entry.wallet) if not wallet: wallet = await get_wallet_for_key(entry.wallet, "invoice") @@ -40,7 +36,7 @@ async def api_targets_set( detail=f"Invalid wallet '{entry.wallet}'.", ) - if wallet.id == wallet.wallet.id: + if wallet.id == wal.wallet.id: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail="Can't split to itself.", @@ -53,7 +49,7 @@ async def api_targets_set( ) targets.append( - Target(wallet.id, wallet.wallet.id, entry.percent, entry.alias or "") + Target(wallet=wallet.id, source=wal.wallet.id, percent=entry.percent, alias=entry.alias) ) percent_sum = sum([target.percent for target in targets]) @@ -63,5 +59,5 @@ async def api_targets_set( detail="Splitting over 100%.", ) - await set_targets(wallet.wallet.id, targets) + await set_targets(wal.wallet.id, targets) return "" From 304001632315afc1eca7e95184577cb42bc650af Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Tue, 19 Oct 2021 16:12:03 +0100 Subject: [PATCH 64/75] events untested commit --- lnbits/extensions/events/README.md | 33 ++ lnbits/extensions/events/__init__.py | 19 + lnbits/extensions/events/config.json | 6 + lnbits/extensions/events/crud.py | 159 ++++++ lnbits/extensions/events/migrations.py | 91 +++ lnbits/extensions/events/models.py | 41 ++ .../events/templates/events/_api_docs.html | 23 + .../events/templates/events/display.html | 207 +++++++ .../events/templates/events/error.html | 35 ++ .../events/templates/events/index.html | 538 ++++++++++++++++++ .../events/templates/events/register.html | 173 ++++++ .../events/templates/events/ticket.html | 45 ++ lnbits/extensions/events/views.py | 107 ++++ lnbits/extensions/events/views_api.py | 211 +++++++ 14 files changed, 1688 insertions(+) create mode 100644 lnbits/extensions/events/README.md create mode 100644 lnbits/extensions/events/__init__.py create mode 100644 lnbits/extensions/events/config.json create mode 100644 lnbits/extensions/events/crud.py create mode 100644 lnbits/extensions/events/migrations.py create mode 100644 lnbits/extensions/events/models.py create mode 100644 lnbits/extensions/events/templates/events/_api_docs.html create mode 100644 lnbits/extensions/events/templates/events/display.html create mode 100644 lnbits/extensions/events/templates/events/error.html create mode 100644 lnbits/extensions/events/templates/events/index.html create mode 100644 lnbits/extensions/events/templates/events/register.html create mode 100644 lnbits/extensions/events/templates/events/ticket.html create mode 100644 lnbits/extensions/events/views.py create mode 100644 lnbits/extensions/events/views_api.py diff --git a/lnbits/extensions/events/README.md b/lnbits/extensions/events/README.md new file mode 100644 index 00000000..11b62fec --- /dev/null +++ b/lnbits/extensions/events/README.md @@ -0,0 +1,33 @@ +# Events + +## Sell tickets for events and use the built-in scanner for registering attendants + +Events alows you to make tickets for an event. Each ticket is in the form of a uniqque QR code. After registering, and paying for ticket, the user gets a QR code to present at registration/entrance. + +Events includes a shareable ticket scanner, which can be used to register attendees. + +## Usage + +1. Create an event\ + ![create event](https://i.imgur.com/dadK1dp.jpg) +2. Fill out the event information: + + - event name + - wallet (normally there's only one) + - event information + - closing date for event registration + - begin and end date of the event + + ![event info](https://imgur.com/KAv68Yr.jpg) + +3. Share the event registration link\ + ![event ticket](https://imgur.com/AQWUOBY.jpg) + + - ticket example\ + ![ticket example](https://i.imgur.com/trAVSLd.jpg) + + - QR code ticket, presented after invoice paid, to present at registration\ + ![event ticket](https://i.imgur.com/M0ROM82.jpg) + +4. Use the built-in ticket scanner to validate registered, and paid, attendees\ + ![ticket scanner](https://i.imgur.com/zrm9202.jpg) diff --git a/lnbits/extensions/events/__init__.py b/lnbits/extensions/events/__init__.py new file mode 100644 index 00000000..da29358b --- /dev/null +++ b/lnbits/extensions/events/__init__.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter + +from lnbits.db import Database +from lnbits.helpers import template_renderer + +db = Database("ext_events") + + +events_ext: APIRouter = APIRouter( + prefix="/events", + tags=["Events"] +) + +def events_renderer(): + return template_renderer(["lnbits/extensions/events/templates"]) + +from .views import * # noqa +from .views_api import * # noqa + diff --git a/lnbits/extensions/events/config.json b/lnbits/extensions/events/config.json new file mode 100644 index 00000000..6bc144ab --- /dev/null +++ b/lnbits/extensions/events/config.json @@ -0,0 +1,6 @@ +{ + "name": "Events", + "short_description": "Sell and register event tickets", + "icon": "local_activity", + "contributors": ["benarc"] +} diff --git a/lnbits/extensions/events/crud.py b/lnbits/extensions/events/crud.py new file mode 100644 index 00000000..4a24b797 --- /dev/null +++ b/lnbits/extensions/events/crud.py @@ -0,0 +1,159 @@ +from typing import List, Optional, Union + +from lnbits.helpers import urlsafe_short_hash + +from . import db +from .models import CreateEvent, Events, Tickets + +# TICKETS + + +async def create_ticket( + payment_hash: str, wallet: str, event: str, name: str, email: str +) -> Tickets: + await db.execute( + """ + INSERT INTO events.ticket (id, wallet, event, name, email, registered, paid) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + (payment_hash, wallet, event, name, email, False, False), + ) + + ticket = await get_ticket(payment_hash) + assert ticket, "Newly created ticket couldn't be retrieved" + return ticket + + +async def set_ticket_paid(payment_hash: str) -> Tickets: + row = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (payment_hash,)) + if row[6] != True: + await db.execute( + """ + UPDATE events.ticket + SET paid = true + WHERE id = ? + """, + (payment_hash,), + ) + + eventdata = await get_event(row[2]) + assert eventdata, "Couldn't get event from ticket being paid" + + sold = eventdata.sold + 1 + amount_tickets = eventdata.amount_tickets - 1 + await db.execute( + """ + UPDATE events.events + SET sold = ?, amount_tickets = ? + WHERE id = ? + """, + (sold, amount_tickets, row[2]), + ) + + ticket = await get_ticket(payment_hash) + assert ticket, "Newly updated ticket couldn't be retrieved" + return ticket + + +async def get_ticket(payment_hash: str) -> Optional[Tickets]: + row = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (payment_hash,)) + return Tickets(**row) if row else None + + +async def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM events.ticket WHERE wallet IN ({q})", (*wallet_ids,) + ) + return [Tickets(**row) for row in rows] + + +async def delete_ticket(payment_hash: str) -> None: + await db.execute("DELETE FROM events.ticket WHERE id = ?", (payment_hash,)) + + +# EVENTS + + +async def create_event( + data: CreateEvent +) -> Events: + event_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO events.events (id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + event_id, + data.wallet, + data.name, + data.info, + data.closing_date, + data.event_start_date, + data.event_end_date, + data.amount_tickets, + data.price_per_ticket, + 0, + ), + ) + + event = await get_event(event_id) + assert event, "Newly created event couldn't be retrieved" + return event + + +async def update_event(event_id: str, **kwargs) -> Events: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE events.events SET {q} WHERE id = ?", (*kwargs.values(), event_id) + ) + event = await get_event(event_id) + assert event, "Newly updated event couldn't be retrieved" + return event + + +async def get_event(event_id: str) -> Optional[Events]: + row = await db.fetchone("SELECT * FROM events.events WHERE id = ?", (event_id,)) + return Events(**row) if row else None + + +async def get_events(wallet_ids: Union[str, List[str]]) -> List[Events]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM events.events WHERE wallet IN ({q})", (*wallet_ids,) + ) + + return [Events(**row) for row in rows] + + +async def delete_event(event_id: str) -> None: + await db.execute("DELETE FROM events.events WHERE id = ?", (event_id,)) + + +# EVENTTICKETS + + +async def get_event_tickets(event_id: str, wallet_id: str) -> List[Tickets]: + rows = await db.fetchall( + "SELECT * FROM events.ticket WHERE wallet = ? AND event = ?", + (wallet_id, event_id), + ) + return [Tickets(**row) for row in rows] + + +async def reg_ticket(ticket_id: str) -> List[Tickets]: + await db.execute( + "UPDATE events.ticket SET registered = ? WHERE id = ?", (True, ticket_id) + ) + ticket = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (ticket_id,)) + rows = await db.fetchall( + "SELECT * FROM events.ticket WHERE event = ?", (ticket[1],) + ) + return [Tickets(**row) for row in rows] diff --git a/lnbits/extensions/events/migrations.py b/lnbits/extensions/events/migrations.py new file mode 100644 index 00000000..d8f3d94e --- /dev/null +++ b/lnbits/extensions/events/migrations.py @@ -0,0 +1,91 @@ +async def m001_initial(db): + + await db.execute( + """ + CREATE TABLE events.events ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + name TEXT NOT NULL, + info TEXT NOT NULL, + closing_date TEXT NOT NULL, + event_start_date TEXT NOT NULL, + event_end_date TEXT NOT NULL, + amount_tickets INTEGER NOT NULL, + price_per_ticket INTEGER NOT NULL, + sold INTEGER NOT NULL, + time TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ + ); + """ + ) + + await db.execute( + """ + CREATE TABLE events.tickets ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + event TEXT NOT NULL, + name TEXT NOT NULL, + email TEXT NOT NULL, + registered BOOLEAN NOT NULL, + time TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ + ); + """ + ) + + +async def m002_changed(db): + + await db.execute( + """ + CREATE TABLE events.ticket ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + event TEXT NOT NULL, + name TEXT NOT NULL, + email TEXT NOT NULL, + registered BOOLEAN NOT NULL, + paid BOOLEAN NOT NULL, + time TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ + ); + """ + ) + + for row in [list(row) for row in await db.fetchall("SELECT * FROM events.tickets")]: + usescsv = "" + + for i in range(row[5]): + if row[7]: + usescsv += "," + str(i + 1) + else: + usescsv += "," + str(1) + usescsv = usescsv[1:] + await db.execute( + """ + INSERT INTO events.ticket ( + id, + wallet, + event, + name, + email, + registered, + paid + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + row[0], + row[1], + row[2], + row[3], + row[4], + row[5], + True, + ), + ) + await db.execute("DROP TABLE events.tickets") diff --git a/lnbits/extensions/events/models.py b/lnbits/extensions/events/models.py new file mode 100644 index 00000000..c775382f --- /dev/null +++ b/lnbits/extensions/events/models.py @@ -0,0 +1,41 @@ +from fastapi.param_functions import Query +from pydantic import BaseModel + + +class CreateEvent(BaseModel): + wallet: str + name: str + info: str + closing_date: str + event_start_date: str + event_end_date: str + amount_tickets: int = Query(..., ge=0) + price_per_ticket: int = Query(..., ge=0) + +class CreateTicket(BaseModel): + name: str + email: str + +class Events(BaseModel): + id: str + wallet: str + name: str + info: str + closing_date: str + event_start_date: str + event_end_date: str + amount_tickets: int + price_per_ticket: int + sold: int + time: int + + +class Tickets(BaseModel): + id: str + wallet: str + event: str + name: str + email: str + registered: bool + paid: bool + time: int diff --git a/lnbits/extensions/events/templates/events/_api_docs.html b/lnbits/extensions/events/templates/events/_api_docs.html new file mode 100644 index 00000000..a5c82174 --- /dev/null +++ b/lnbits/extensions/events/templates/events/_api_docs.html @@ -0,0 +1,23 @@ + + + +
+ Events: Sell and register ticket waves for an event +
+

+ Events alows you to make a wave of tickets for an event, each ticket is + in the form of a unqiue QRcode, which the user presents at registration. + Events comes with a shareable ticket scanner, which can be used to + register attendees.
+ + Created by, Ben Arc + +

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

{{ event_name }}

+
+
{{ event_info }}
+
+ + + + +
+ Submit + Cancel +
+
+
+
+ + +
+ Link to your ticket! +

+

You'll be redirected in a few moments...

+
+
+
+ + + + + + +
+ Copy invoice + Close +
+
+
+
+ +{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/lnbits/extensions/events/templates/events/error.html b/lnbits/extensions/events/templates/events/error.html new file mode 100644 index 00000000..f231177b --- /dev/null +++ b/lnbits/extensions/events/templates/events/error.html @@ -0,0 +1,35 @@ +{% extends "public.html" %} {% block page %} +
+
+ + +
+

{{ event_name }} error

+
+ + +
{{ event_error }}
+
+
+
+
+
+ + {% endblock %} {% block scripts %} + + + + {% endblock %} +
diff --git a/lnbits/extensions/events/templates/events/index.html b/lnbits/extensions/events/templates/events/index.html new file mode 100644 index 00000000..1ad3d885 --- /dev/null +++ b/lnbits/extensions/events/templates/events/index.html @@ -0,0 +1,538 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + New Event + + + + + +
+
+
Events
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + +
+
+
Tickets
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+
+ + +
+ {{SITE_TITLE}} Events extension +
+
+ + + {% include "events/_api_docs.html" %} + +
+
+ + + + +
+
+ +
+
+ + +
+
+ + +
+
Ticket closing date
+
+ +
+
+ +
+
Event begins
+
+ +
+
+ +
+
Event ends
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ Update Event + Create Event + Cancel +
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/events/templates/events/register.html b/lnbits/extensions/events/templates/events/register.html new file mode 100644 index 00000000..4dff9afb --- /dev/null +++ b/lnbits/extensions/events/templates/events/register.html @@ -0,0 +1,173 @@ +{% extends "public.html" %} {% block page %} + +
+
+ + +
+

{{ event_name }} Registration

+
+ +
+ + Scan ticket +
+
+
+ + + + + {% raw %} + + + {% endraw %} + + + +
+ + + +
+ +
+
+ Cancel +
+
+
+
+{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/lnbits/extensions/events/templates/events/ticket.html b/lnbits/extensions/events/templates/events/ticket.html new file mode 100644 index 00000000..a53f834f --- /dev/null +++ b/lnbits/extensions/events/templates/events/ticket.html @@ -0,0 +1,45 @@ +{% extends "public.html" %} {% block page %} +
+
+ + +
+

{{ ticket_name }} Ticket

+
+
+ Bookmark, print or screenshot this page,
+ and present it for registration! +
+
+ + +
+ + Print +
+
+
+
+
+{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/lnbits/extensions/events/views.py b/lnbits/extensions/events/views.py new file mode 100644 index 00000000..46aba428 --- /dev/null +++ b/lnbits/extensions/events/views.py @@ -0,0 +1,107 @@ +from datetime import date, datetime +from http import HTTPStatus + +from fastapi import Request +from fastapi.params import Depends +from fastapi.templating import Jinja2Templates +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse + +from lnbits.core.models import User +from lnbits.decorators import check_user_exists + +from . import events_ext, events_renderer +from .crud import get_event, get_ticket + +templates = Jinja2Templates(directory="templates") + + +@events_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return events_renderer.TemplateResponse("events/index.html", {"request": request, "user": user.dict()}) + + +@events_ext.get("/{event_id}", response_class=HTMLResponse) +async def display(request: Request, event_id): + event = await get_event(event_id) + if not event: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." + ) + + if event.amount_tickets < 1: + return events_renderer.TemplateResponse( + "events/error.html", + { + "request": request, + "event_name": event.name, + "event_error": "Sorry, tickets are sold out :(" + } + ) + datetime_object = datetime.strptime(event.closing_date, "%Y-%m-%d").date() + if date.today() > datetime_object: + return events_renderer.TemplateResponse( + "events/error.html", + { + "request": request, + "event_name": event.name, + "event_error": "Sorry, ticket closing date has passed :(" + } + ) + + return events_renderer.TemplateResponse( + "events/display.html", + { + "request": request, + "event_id": event_id, + "event_name": event.name, + "event_info": event.info, + "event_price": event.price_per_ticket, + } + + ) + + +@events_ext.get("/ticket/{ticket_id}", response_class=HTMLResponse) +async def ticket(request: Request, ticket_id): + ticket = await get_ticket(ticket_id) + if not ticket: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist." + ) + + event = await get_event(ticket.event) + if not event: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." + ) + + return events_renderer.TemplateResponse( + "events/ticket.html", + { + "request": request, + "ticket_id": ticket_id, + "ticket_name": event.name, + "ticket_info": event.info, + + } + ) + + +@events_ext.get("/register/{event_id}", response_class=HTMLResponse) +async def register(request: Request, event_id): + event = await get_event(event_id) + if not event: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." + ) + + return events_renderer.TemplateResponse( + "events/register.html", + { + "request": request, + "event_id": event_id, + "event_name": event.name, + "wallet_id": event.wallet, + } + ) diff --git a/lnbits/extensions/events/views_api.py b/lnbits/extensions/events/views_api.py new file mode 100644 index 00000000..2983b45b --- /dev/null +++ b/lnbits/extensions/events/views_api.py @@ -0,0 +1,211 @@ +from http import HTTPStatus + +from fastapi.param_functions import Query +from fastapi.params import Depends +from starlette.exceptions import HTTPException +from starlette.requests import Request + +from lnbits.core.crud import get_user, get_wallet +from lnbits.core.services import check_invoice_status, create_invoice +from lnbits.decorators import WalletTypeInfo, get_key_type +from lnbits.extensions.events.models import CreateEvent, CreateTicket + +from . import events_ext +from .crud import ( + create_event, + create_ticket, + delete_event, + delete_ticket, + get_event, + get_event_tickets, + get_events, + get_ticket, + get_tickets, + reg_ticket, + set_ticket_paid, + update_event, +) + +# Events + + +@events_ext.get("/api/v1/events") +async def api_events( + r: Request, + all_wallets: bool = Query(False), + wallet: WalletTypeInfo = Depends(get_key_type), +): + wallet_ids = [wallet.wallet.id] + + if all_wallets: + wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids + + return [event.dict() for event in await get_events(wallet_ids)] + +@events_ext.post("/api/v1/events") +@events_ext.put("/api/v1/events/") +async def api_event_create(data: CreateEvent, event_id=None, wallet: WalletTypeInfo = Depends(get_key_type)): + if event_id: + event = await get_event(event_id) + if not event: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"Event does not exist." + ) + + if event.wallet != wallet.wallet.id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail=f"Not your event." + ) + event = await update_event(event_id, **data) + else: + event = await create_event(**data) + + return event.dict() + + +@events_ext.delete("/api/v1/events/{event_id}") +async def api_form_delete(event_id, wallet: WalletTypeInfo = Depends(get_key_type)): + event = await get_event(event_id) + if not event: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"Event does not exist." + ) + + if event.wallet != wallet.wallet.id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail=f"Not your event." + ) + + await delete_event(event_id) + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + + +#########Tickets########## + + +@events_ext.get("/api/v1/tickets") +async def api_tickets( + r: Request, + all_wallets: bool = Query(False), + wallet: WalletTypeInfo = Depends(get_key_type), +): + wallet_ids = [wallet.wallet.id] + + if all_wallets: + wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids + + return [ticket.dict() for ticket in await get_tickets(wallet_ids)] + + +@events_ext.post("/api/v1/tickets/{event_id}/{sats}") +async def api_ticket_make_ticket(event_id, sats, data: CreateTicket): + event = await get_event(event_id) + if not event: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"Event does not exist." + ) + try: + payment_hash, payment_request = await create_invoice( + wallet_id=event.wallet, + amount=int(sats), + memo=f"{event_id}", + extra={"tag": "events"}, + ) + except Exception as e: + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) + + ticket = await create_ticket( + payment_hash=payment_hash, wallet=event.wallet, event=event_id, name=data.name, email=data.email + ) + + if not ticket: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"Event could not be fetched." + ) + + return {"payment_hash": payment_hash, "payment_request": payment_request} + + +@events_ext.get("/api/v1/tickets/{payment_hash}") +async def api_ticket_send_ticket(payment_hash): + ticket = await get_ticket(payment_hash) + + try: + status = await check_invoice_status(ticket.wallet, payment_hash) + is_paid = not status.pending + + except Exception: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Not paid") + + if is_paid: + wallet = await get_wallet(ticket.wallet) + payment = await wallet.get_payment(payment_hash) + await payment.set_pending(False) + ticket = await set_ticket_paid(payment_hash=payment_hash) + + return {"paid": True, "ticket_id": ticket.id} + + return {"paid": False} + + +@events_ext.delete("/api/v1/tickets/{ticket_id}") +async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_type)): + ticket = await get_ticket(ticket_id) + if not ticket: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"Ticket does not exist." + ) + + if ticket.wallet != wallet.wallet.id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail=f"Not your ticket." + ) + + await delete_ticket(ticket_id) + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + + + +# Event Tickets + + +@events_ext.get("/api/v1/eventtickets/{wallet_id}/{event_id}") +async def api_event_tickets(wallet_id, event_id): + return [ + ticket.dict() + for ticket in await get_event_tickets( + wallet_id=wallet_id, event_id=event_id + ) + ] + + +@events_ext.get("/api/v1/register/ticket/{ticket_id}") +async def api_event_register_ticket(ticket_id): + ticket = await get_ticket(ticket_id) + if not ticket: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Ticket does not exist." + ) + + if not ticket.paid: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Ticket not paid for." + ) + + if ticket.registered == True: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Ticket already registered" + ) + + return [ticket.dict() for ticket in await reg_ticket(ticket_id)] From 41abbb44f7497ba5bdb5213ce8e971e6c1b405fc Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Tue, 19 Oct 2021 17:01:57 +0100 Subject: [PATCH 65/75] added new error message (detail) --- lnbits/static/js/base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js index fec75796..13f68388 100644 --- a/lnbits/static/js/base.js +++ b/lnbits/static/js/base.js @@ -225,7 +225,7 @@ window.LNbits = { Quasar.plugins.Notify.create({ timeout: 5000, type: types[error.response.status] || 'warning', - message: error.response.data.message || null, + message: error.response.data.message || error.response.data.detail || null, caption: [error.response.status, ' ', error.response.statusText] .join('') From facd94f59e73567831286636144159e7c65fb8bc Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Tue, 19 Oct 2021 17:02:22 +0100 Subject: [PATCH 66/75] fix create and update --- lnbits/extensions/events/templates/events/index.html | 2 +- lnbits/extensions/events/views.py | 12 ++++++------ lnbits/extensions/events/views_api.py | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lnbits/extensions/events/templates/events/index.html b/lnbits/extensions/events/templates/events/index.html index 1ad3d885..c2d81960 100644 --- a/lnbits/extensions/events/templates/events/index.html +++ b/lnbits/extensions/events/templates/events/index.html @@ -427,7 +427,7 @@ LNbits.api .request( 'GET', - '/events/api/v1/events?all_wallets', + '/events/api/v1/events?all_wallets=true', this.g.user.wallets[0].inkey ) .then(function (response) { diff --git a/lnbits/extensions/events/views.py b/lnbits/extensions/events/views.py index 46aba428..a80f7806 100644 --- a/lnbits/extensions/events/views.py +++ b/lnbits/extensions/events/views.py @@ -18,7 +18,7 @@ templates = Jinja2Templates(directory="templates") @events_ext.get("/", response_class=HTMLResponse) async def index(request: Request, user: User = Depends(check_user_exists)): - return events_renderer.TemplateResponse("events/index.html", {"request": request, "user": user.dict()}) + return events_renderer().TemplateResponse("events/index.html", {"request": request, "user": user.dict()}) @events_ext.get("/{event_id}", response_class=HTMLResponse) @@ -30,7 +30,7 @@ async def display(request: Request, event_id): ) if event.amount_tickets < 1: - return events_renderer.TemplateResponse( + return events_renderer().TemplateResponse( "events/error.html", { "request": request, @@ -40,7 +40,7 @@ async def display(request: Request, event_id): ) datetime_object = datetime.strptime(event.closing_date, "%Y-%m-%d").date() if date.today() > datetime_object: - return events_renderer.TemplateResponse( + return events_renderer().TemplateResponse( "events/error.html", { "request": request, @@ -49,7 +49,7 @@ async def display(request: Request, event_id): } ) - return events_renderer.TemplateResponse( + return events_renderer().TemplateResponse( "events/display.html", { "request": request, @@ -76,7 +76,7 @@ async def ticket(request: Request, ticket_id): status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." ) - return events_renderer.TemplateResponse( + return events_renderer().TemplateResponse( "events/ticket.html", { "request": request, @@ -96,7 +96,7 @@ async def register(request: Request, event_id): status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." ) - return events_renderer.TemplateResponse( + return events_renderer().TemplateResponse( "events/register.html", { "request": request, diff --git a/lnbits/extensions/events/views_api.py b/lnbits/extensions/events/views_api.py index 2983b45b..5dae31e1 100644 --- a/lnbits/extensions/events/views_api.py +++ b/lnbits/extensions/events/views_api.py @@ -43,7 +43,7 @@ async def api_events( return [event.dict() for event in await get_events(wallet_ids)] @events_ext.post("/api/v1/events") -@events_ext.put("/api/v1/events/") +@events_ext.put("/api/v1/events/{event_id}") async def api_event_create(data: CreateEvent, event_id=None, wallet: WalletTypeInfo = Depends(get_key_type)): if event_id: event = await get_event(event_id) @@ -58,9 +58,9 @@ async def api_event_create(data: CreateEvent, event_id=None, wallet: WalletTypeI status_code=HTTPStatus.FORBIDDEN, detail=f"Not your event." ) - event = await update_event(event_id, **data) + event = await update_event(event_id, **data.dict()) else: - event = await create_event(**data) + event = await create_event(data=data) return event.dict() From fc5885d52bc6405fbcc669e58e4f294742585b91 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Tue, 19 Oct 2021 20:54:02 +0100 Subject: [PATCH 67/75] creating but not fetching --- lnbits/extensions/satsdice/crud.py | 4 +++- lnbits/extensions/satsdice/models.py | 4 ++-- lnbits/extensions/satsdice/templates/satsdice/index.html | 3 +-- lnbits/extensions/satsdice/views.py | 2 +- lnbits/extensions/satsdice/views_api.py | 5 ++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lnbits/extensions/satsdice/crud.py b/lnbits/extensions/satsdice/crud.py index e4476a11..dd7d484a 100644 --- a/lnbits/extensions/satsdice/crud.py +++ b/lnbits/extensions/satsdice/crud.py @@ -15,7 +15,9 @@ from .models import ( from lnbits.helpers import urlsafe_short_hash -async def create_satsdice_pay(data: CreateSatsDiceLink,) -> satsdiceLink: +async def create_satsdice_pay( + data: CreateSatsDiceLink, +) -> satsdiceLink: satsdice_id = urlsafe_short_hash() await db.execute( """ diff --git a/lnbits/extensions/satsdice/models.py b/lnbits/extensions/satsdice/models.py index b38d7522..21a7a12a 100644 --- a/lnbits/extensions/satsdice/models.py +++ b/lnbits/extensions/satsdice/models.py @@ -27,8 +27,8 @@ class satsdiceLink(BaseModel): base_url: str open_time: int - def lnurl(self, req: Request) -> Lnurl: - return lnurl_encode(req.url_for("satsdice.lnurlp_response", item_id=self.id)) + def lnurl(self, req: Request) -> str: + return lnurl_encode(req.url_for("satsdice.lnurlp_response", link_id=self.id)) @classmethod def from_row(cls, row: Row) -> "satsdiceLink": diff --git a/lnbits/extensions/satsdice/templates/satsdice/index.html b/lnbits/extensions/satsdice/templates/satsdice/index.html index b9c1fae9..a5ec243d 100644 --- a/lnbits/extensions/satsdice/templates/satsdice/index.html +++ b/lnbits/extensions/satsdice/templates/satsdice/index.html @@ -359,6 +359,7 @@ }, openQrCodeDialog(linkId) { var link = _.findWhere(this.payLinks, {id: linkId}) + console.log(link) if (link.currency) this.updateFiatRate(link.currency) this.qrCodeDialog.data = { @@ -512,8 +513,6 @@ } }, created() { - console.log('this.multiValue') - console.log(this.g.user) if (this.g.user.wallets.length) { var getPayLinks = this.getPayLinks getPayLinks() diff --git a/lnbits/extensions/satsdice/views.py b/lnbits/extensions/satsdice/views.py index fe0ad481..2f2f74bc 100644 --- a/lnbits/extensions/satsdice/views.py +++ b/lnbits/extensions/satsdice/views.py @@ -29,7 +29,7 @@ from fastapi.param_functions import Query templates = Jinja2Templates(directory="templates") -@satsdice_ext.get("/", response_class=HTMLResponse) +@satsdice_ext.get("/") async def index(request: Request, user: User = Depends(check_user_exists)): return satsdice_renderer().TemplateResponse( "satsdice/index.html", {"request": request, "user": user.dict()} diff --git a/lnbits/extensions/satsdice/views_api.py b/lnbits/extensions/satsdice/views_api.py index 3c7471de..315d823c 100644 --- a/lnbits/extensions/satsdice/views_api.py +++ b/lnbits/extensions/satsdice/views_api.py @@ -40,9 +40,8 @@ async def api_links( try: links = await get_satsdice_pays(wallet_ids) - print(links[0]) - return [{link.dict(), {"lnurl": link.lnurl(request)}} for link in links] + return [{**link.dict(), **{"lnurl": link.lnurl(request)}} for link in links] except LnurlInvalidUrl: raise HTTPException( status_code=HTTPStatus.UPGRADE_REQUIRED, @@ -99,7 +98,7 @@ async def api_link_create_or_update( data.wallet_id = wallet.wallet.id link = await create_satsdice_pay(data) - return {link.dict(), {"lnurl": link.lnurl}} + return {**link.dict(), **{"lnurl": link.lnurl}} @satsdice_ext.delete("/api/v1/links/{link_id}") From 4a9b5840ab7f23fa99d0b5cb01eb0835b4cf3b20 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Tue, 19 Oct 2021 21:08:12 +0100 Subject: [PATCH 68/75] Ngrok init --- Pipfile | 1 + Pipfile.lock | 400 +++++++++--------- lnbits/extensions/ngrok/README.md | 24 ++ lnbits/extensions/ngrok/__init__.py | 18 + lnbits/extensions/ngrok/config.json | 6 + lnbits/extensions/ngrok/migrations.py | 11 + .../ngrok/templates/ngrok/index.html | 53 +++ lnbits/extensions/ngrok/views.py | 43 ++ 8 files changed, 347 insertions(+), 209 deletions(-) create mode 100644 lnbits/extensions/ngrok/README.md create mode 100644 lnbits/extensions/ngrok/__init__.py create mode 100644 lnbits/extensions/ngrok/config.json create mode 100644 lnbits/extensions/ngrok/migrations.py create mode 100644 lnbits/extensions/ngrok/templates/ngrok/index.html create mode 100644 lnbits/extensions/ngrok/views.py diff --git a/Pipfile b/Pipfile index af3e4174..47718583 100644 --- a/Pipfile +++ b/Pipfile @@ -28,6 +28,7 @@ fastapi = "*" uvicorn = {extras = ["standard"], version = "*"} sse-starlette = "*" jinja2 = "3.0.1" +pyngrok = "*" [dev-packages] black = "==20.8b1" diff --git a/Pipfile.lock b/Pipfile.lock index 907c539e..25091725 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "97473b3cb250742ebabd8c3a71d4e4c42f8feeaff49dd4542cae24429f096535" + "sha256": "9c0e70708a7767ec1f6c4b3df1a0926184220014ab67ff82d4f352c634918085" }, "pipfile-spec": 6, "requires": { @@ -26,11 +26,11 @@ }, "anyio": { "hashes": [ - "sha256:85913b4e2fec030e8c72a8f9f98092eeb9e25847a6e00d567751b77e34f856fe", - "sha256:d7c604dd491eca70e19c78664d685d5e4337612d574419d503e76f5d7d1590bd" + "sha256:4fd09a25ab7fa01d34512b7249e366cd10358cdafc95022c7ff8c8f8a5026d66", + "sha256:67da67b5b21f96b9d3d65daa6ea99f5d5282cb09f50eb4456f8fb51dffefc3ff" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.3.1" + "version": "==3.3.4" }, "asgiref": { "hashes": [ @@ -63,7 +63,7 @@ "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899", "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981" ], - "markers": "python_version >= '3.5'", + "markers": "python_full_version >= '3.5.0'", "version": "==1.2.0" }, "bitstring": { @@ -84,26 +84,26 @@ }, "certifi": { "hashes": [ - "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", - "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" ], - "version": "==2021.5.30" + "version": "==2021.10.8" }, "charset-normalizer": { "hashes": [ - "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6", - "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f" + "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", + "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" ], - "markers": "python_version >= '3.5'", - "version": "==2.0.6" + "markers": "python_full_version >= '3.5.0'", + "version": "==2.0.7" }, "click": { "hashes": [ - "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", - "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" ], "markers": "python_version >= '3.6'", - "version": "==8.0.1" + "version": "==8.0.3" }, "ecdsa": { "hashes": [ @@ -115,26 +115,26 @@ }, "embit": { "hashes": [ - "sha256:992332bd89af6e2d027e26fe437eb14aa33997db08c882c49064d49c3e6f4ab9" + "sha256:f6484bc495b45da27f3eb7fbe21a24c00cd72c0ab83c6e195660cf17db5cb5e2" ], "index": "pypi", - "version": "==0.4.9" + "version": "==0.4.10" }, "environs": { "hashes": [ - "sha256:72b867ff7b553076cdd90f3ee01ecc1cf854987639c9c459f0ed0d3d44ae490c", - "sha256:ee5466156b50fe03aa9fec6e720feea577b5bf515d7f21b2c46608272557ba26" + "sha256:7412eca2996027a0a1eafd89bbfec872568e7b4ca75fc980817bfd7788cb5a1f", + "sha256:eecf57fb1b91f1166a8a16344a3fd12ea55b7a0f233c906d86506bdb40738a0f" ], "index": "pypi", - "version": "==9.3.3" + "version": "==9.3.4" }, "fastapi": { "hashes": [ - "sha256:644bb815bae326575c4b2842469fb83053a4b974b82fa792ff9283d17fbbd99d", - "sha256:94d2820906c36b9b8303796fb7271337ec89c74223229e3cfcf056b5a7d59e23" + "sha256:66da43cfe5185ea1df99552acffd201f1832c6b364e0f4136c0a99f933466ced", + "sha256:a36d5f2fad931aa3575c07a3472c784e81f3e664e3bb5c8b9c88d0ec1104f59c" ], "index": "pypi", - "version": "==0.68.1" + "version": "==0.70.0" }, "h11": { "hashes": [ @@ -174,34 +174,26 @@ }, "httpx": { "hashes": [ - "sha256:92ecd2c00c688b529eda11cedb15161eaf02dee9116712f621c70d9a40b2cdd0", - "sha256:9bd728a6c5ec0a9e243932a9983d57d3cc4a87bb4f554e1360fce407f78f9435" + "sha256:09606d630f070d07f9ff28104fbcea429ea0014c1e89ac90b4d8de8286c40e7b", + "sha256:33af5aad9bdc82ef1fc89219c1e36f5693bf9cd0ebe330884df563445682c0f8" ], "index": "pypi", - "version": "==0.19.0" + "version": "==0.20.0" }, "idna": { "hashes": [ - "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", - "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "version": "==3.2" - }, - "importlib-metadata": { - "hashes": [ - "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15", - "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1" - ], - "markers": "python_version < '3.8'", - "version": "==4.8.1" + "version": "==3.3" }, "jinja2": { "hashes": [ - "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", - "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" + "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45", + "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c" ], "index": "pypi", - "version": "==3.0.1" + "version": "==3.0.2" }, "lnurl": { "hashes": [ @@ -273,11 +265,11 @@ }, "marshmallow": { "hashes": [ - "sha256:c67929438fd73a2be92128caa0325b1b5ed8b626d91a094d2f7f2771bf1f1c0e", - "sha256:dd4724335d3c2b870b641ffe4a2f8728a1380cd2e7e2312756715ffeaa82b842" + "sha256:6d00e42d6d6289f8cd3e77618a01689d57a078fe324ee579b00fa206d32e9b07", + "sha256:bba1a940985c052c5cc7849f97da196ebc81f3b85ec10c56ef1f3228aa9cbe74" ], - "markers": "python_version >= '3.5'", - "version": "==3.13.0" + "markers": "python_version >= '3.6'", + "version": "==3.14.0" }, "outcome": { "hashes": [ @@ -297,12 +289,16 @@ "sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a", "sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e", "sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d", + "sha256:24b0b6688b9f31a911f2361fe818492650795c9e5d3a1bc647acbd7440142a4f", "sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed", "sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a", "sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140", "sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32", + "sha256:542875f62bc56e91c6eac05a0deadeae20e1730be4c6334d8f04c944fcd99759", "sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31", + "sha256:661509f51531ec125e52357a489ea3806640d0ca37d9dada461ffc69ee1e7b6e", "sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a", + "sha256:736b8797b58febabb85494142c627bd182b50d2a7ec65322983e71065ad3034c", "sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917", "sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf", "sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7", @@ -314,6 +310,7 @@ "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76", "sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4", "sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f", + "sha256:d92272c7c16e105788efe2cfa5d680f07e34e0c29b03c1908f8636f55d5f915a", "sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34", "sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce", "sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a", @@ -350,6 +347,13 @@ "markers": "python_full_version >= '3.6.1'", "version": "==1.8.2" }, + "pyngrok": { + "hashes": [ + "sha256:4d03f44a69c3cbc168b17377956a9edcf723e77dbc864eba34c272db15da443c" + ], + "index": "pypi", + "version": "==5.1.0" + }, "pypng": { "hashes": [ "sha256:76f8a1539ec56451da7ab7121f12a361969fe0f2d48d703d198ce2a99d6c5afd" @@ -374,45 +378,49 @@ }, "python-dotenv": { "hashes": [ - "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1", - "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172" + "sha256:14f8185cc8d494662683e6914addcb7e95374771e707601dfc70166946b4c4b8", + "sha256:bbd3da593fc49c249397cbfbcc449cf36cb02e75afc8157fcc6a81df6fb7750a" ], - "markers": "python_version >= '3.5'", - "version": "==0.19.0" + "markers": "python_full_version >= '3.5.0'", + "version": "==0.19.1" }, "pyyaml": { "hashes": [ - "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", - "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", - "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", - "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", - "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", - "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", - "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", - "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", - "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", - "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", - "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", - "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", - "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", - "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", - "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", - "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", - "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", - "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", - "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", - "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", - "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", - "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", - "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", - "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", - "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", - "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", - "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", - "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", - "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" + "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", + "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", + "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", + "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", + "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", + "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", + "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", + "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", + "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", + "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", + "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", + "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", + "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", + "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", + "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", + "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", + "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", + "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", + "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", + "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", + "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", + "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", + "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", + "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", + "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", + "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", + "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" ], - "version": "==5.4.1" + "version": "==6.0" }, "represent": { "hashes": [ @@ -453,7 +461,7 @@ "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" ], - "markers": "python_version >= '3.5'", + "markers": "python_full_version >= '3.5.0'", "version": "==1.2.0" }, "sqlalchemy": { @@ -510,18 +518,19 @@ }, "sse-starlette": { "hashes": [ - "sha256:1c0cc62cc7d021a386dc06a16a9ddc3e2861d19da6bc2e654e65cc111e820456" + "sha256:8adc5bfe8c6ede3cf8f16dc741db813c580a13fd8510ec06d6d3e27987e972d2", + "sha256:bd572df6a74779090a1060759a8c3b94e1aa54240173d76c7d830f03e991875f" ], "index": "pypi", - "version": "==0.6.2" + "version": "==0.9.0" }, "starlette": { "hashes": [ - "sha256:3c8e48e52736b3161e34c9f0e8153b4f32ec5d8995a3ee1d59410d92f75162ed", - "sha256:7d49f4a27f8742262ef1470608c59ddbc66baf37c148e938c7038e6bc7a998aa" + "sha256:38eb24bf705a2c317e15868e384c1b8a12ca396e5a3c3a003db7e667c43f939f", + "sha256:e1904b5d0007aee24bdd3c43994be9b3b729f4f58e740200de1d623f8c3a8870" ], "markers": "python_version >= '3.6'", - "version": "==0.14.2" + "version": "==0.16.0" }, "typing-extensions": { "hashes": [ @@ -600,14 +609,6 @@ "sha256:ff59c6bdb87b31f7e2d596f09353d5a38c8c8ff571b0e2238e8ee2d55ad68465" ], "version": "==10.0" - }, - "zipp": { - "hashes": [ - "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", - "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" - ], - "markers": "python_version >= '3.6'", - "version": "==3.5.0" } }, "develop": { @@ -635,77 +636,53 @@ }, "click": { "hashes": [ - "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", - "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" ], "markers": "python_version >= '3.6'", - "version": "==8.0.1" + "version": "==8.0.3" }, "coverage": { - "hashes": [ - "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", - "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", - "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", - "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", - "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", - "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", - "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", - "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", - "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", - "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", - "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", - "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", - "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", - "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", - "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", - "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", - "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", - "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", - "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", - "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", - "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", - "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", - "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", - "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", - "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", - "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", - "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", - "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", - "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", - "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", - "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", - "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", - "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", - "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", - "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", - "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", - "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", - "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", - "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", - "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", - "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", - "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", - "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", - "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", - "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", - "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", - "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", - "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", - "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", - "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", - "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", - "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" + "extras": [ + "toml" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==5.5" - }, - "importlib-metadata": { "hashes": [ - "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15", - "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1" + "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1", + "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0", + "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9", + "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895", + "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d", + "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe", + "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2", + "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4", + "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce", + "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9", + "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122", + "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7", + "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3", + "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff", + "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149", + "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a", + "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164", + "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1", + "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd", + "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc", + "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f", + "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9", + "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9", + "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0", + "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d", + "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa", + "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7", + "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822", + "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc", + "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7", + "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330", + "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb", + "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24" ], - "markers": "python_version < '3.8'", - "version": "==4.8.1" + "markers": "python_version >= '3.6'", + "version": "==6.0.2" }, "iniconfig": { "hashes": [ @@ -799,57 +776,63 @@ }, "pytest-cov": { "hashes": [ - "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a", - "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7" + "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", + "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" ], "index": "pypi", - "version": "==2.12.1" + "version": "==3.0.0" }, "regex": { "hashes": [ - "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468", - "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354", - "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308", - "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d", - "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc", - "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8", - "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797", - "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2", - "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13", - "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d", - "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a", - "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0", - "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73", - "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1", - "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed", - "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a", - "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b", - "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f", - "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256", - "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb", - "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2", - "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983", - "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb", - "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645", - "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8", - "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a", - "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906", - "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f", - "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c", - "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892", - "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0", - "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e", - "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e", - "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed", - "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c", - "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374", - "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd", - "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791", - "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a", - "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1", - "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759" + "sha256:094a905e87a4171508c2a0e10217795f83c636ccc05ddf86e7272c26e14056ae", + "sha256:09e1031e2059abd91177c302da392a7b6859ceda038be9e015b522a182c89e4f", + "sha256:176796cb7f82a7098b0c436d6daac82f57b9101bb17b8e8119c36eecf06a60a3", + "sha256:19b8f6d23b2dc93e8e1e7e288d3010e58fafed323474cf7f27ab9451635136d9", + "sha256:1abbd95cbe9e2467cac65c77b6abd9223df717c7ae91a628502de67c73bf6838", + "sha256:1ce02f420a7ec3b2480fe6746d756530f69769292eca363218c2291d0b116a01", + "sha256:1f51926db492440e66c89cd2be042f2396cf91e5b05383acd7372b8cb7da373f", + "sha256:26895d7c9bbda5c52b3635ce5991caa90fbb1ddfac9c9ff1c7ce505e2282fb2a", + "sha256:2efd47704bbb016136fe34dfb74c805b1ef5c7313aef3ce6dcb5ff844299f432", + "sha256:36c98b013273e9da5790ff6002ab326e3f81072b4616fd95f06c8fa733d2745f", + "sha256:39079ebf54156be6e6902f5c70c078f453350616cfe7bfd2dd15bdb3eac20ccc", + "sha256:3d52c5e089edbdb6083391faffbe70329b804652a53c2fdca3533e99ab0580d9", + "sha256:45cb0f7ff782ef51bc79e227a87e4e8f24bc68192f8de4f18aae60b1d60bc152", + "sha256:4786dae85c1f0624ac77cb3813ed99267c9adb72e59fdc7297e1cf4d6036d493", + "sha256:51feefd58ac38eb91a21921b047da8644155e5678e9066af7bcb30ee0dca7361", + "sha256:55ef044899706c10bc0aa052f2fc2e58551e2510694d6aae13f37c50f3f6ff61", + "sha256:5e5796d2f36d3c48875514c5cd9e4325a1ca172fc6c78b469faa8ddd3d770593", + "sha256:5f199419a81c1016e0560c39773c12f0bd924c37715bffc64b97140d2c314354", + "sha256:5f55c4804797ef7381518e683249310f7f9646da271b71cb6b3552416c7894ee", + "sha256:6dcf53d35850ce938b4f044a43b33015ebde292840cef3af2c8eb4c860730fff", + "sha256:74e55f8d66f1b41d44bc44c891bcf2c7fad252f8f323ee86fba99d71fd1ad5e3", + "sha256:7f125fce0a0ae4fd5c3388d369d7a7d78f185f904c90dd235f7ecf8fe13fa741", + "sha256:82cfb97a36b1a53de32b642482c6c46b6ce80803854445e19bc49993655ebf3b", + "sha256:88dc3c1acd3f0ecfde5f95c32fcb9beda709dbdf5012acdcf66acbc4794468eb", + "sha256:924079d5590979c0e961681507eb1773a142553564ccae18d36f1de7324e71ca", + "sha256:951be934dc25d8779d92b530e922de44dda3c82a509cdb5d619f3a0b1491fafa", + "sha256:973499dac63625a5ef9dfa4c791aa33a502ddb7615d992bdc89cf2cc2285daa3", + "sha256:981c786293a3115bc14c103086ae54e5ee50ca57f4c02ce7cf1b60318d1e8072", + "sha256:9c070d5895ac6aeb665bd3cd79f673775caf8d33a0b569e98ac434617ecea57d", + "sha256:9e3e2cea8f1993f476a6833ef157f5d9e8c75a59a8d8b0395a9a6887a097243b", + "sha256:9e527ab1c4c7cf2643d93406c04e1d289a9d12966529381ce8163c4d2abe4faf", + "sha256:a37305eb3199d8f0d8125ec2fb143ba94ff6d6d92554c4b8d4a8435795a6eccd", + "sha256:aa0ab3530a279a3b7f50f852f1bab41bc304f098350b03e30a3876b7dd89840e", + "sha256:b04e512eb628ea82ed86eb31c0f7fc6842b46bf2601b66b1356a7008327f7700", + "sha256:b09d3904bf312d11308d9a2867427479d277365b1617e48ad09696fa7dfcdf59", + "sha256:b0f2f874c6a157c91708ac352470cb3bef8e8814f5325e3c5c7a0533064c6a24", + "sha256:b8b6ee6555b6fbae578f1468b3f685cdfe7940a65675611365a7ea1f8d724991", + "sha256:b9b5c215f3870aa9b011c00daeb7be7e1ae4ecd628e9beb6d7e6107e07d81287", + "sha256:c6569ba7b948c3d61d27f04e2b08ebee24fec9ff8e9ea154d8d1e975b175bfa7", + "sha256:e2ec1c106d3f754444abf63b31e5c4f9b5d272272a491fa4320475aba9e8157c", + "sha256:e4204708fa116dd03436a337e8e84261bc8051d058221ec63535c9403a1582a1", + "sha256:ea8de658d7db5987b11097445f2b1f134400e2232cb40e614e5f7b6f5428710e", + "sha256:f540f153c4f5617bc4ba6433534f8916d96366a08797cbbe4132c37b70403e92", + "sha256:fab3ab8aedfb443abb36729410403f0fe7f60ad860c19a979d47fb3eb98ef820", + "sha256:fb2baff66b7d2267e07ef71e17d01283b55b3cc51a81b54cc385e721ae172ba4", + "sha256:fe6ce4f3d3c48f9f402da1ceb571548133d3322003ce01b20d960a82251695d2", + "sha256:ff24897f6b2001c38a805d53b6ae72267025878d35ea225aa24675fbff2dba7f" ], - "version": "==2021.8.28" + "version": "==2021.10.8" }, "toml": { "hashes": [ @@ -859,6 +842,13 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, + "tomli": { + "hashes": [ + "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f", + "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442" + ], + "version": "==1.2.1" + }, "typed-ast": { "hashes": [ "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace", @@ -903,14 +893,6 @@ ], "index": "pypi", "version": "==3.10.0.2" - }, - "zipp": { - "hashes": [ - "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", - "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" - ], - "markers": "python_version >= '3.6'", - "version": "==3.5.0" } } } diff --git a/lnbits/extensions/ngrok/README.md b/lnbits/extensions/ngrok/README.md new file mode 100644 index 00000000..626c788f --- /dev/null +++ b/lnbits/extensions/ngrok/README.md @@ -0,0 +1,24 @@ +

Ngrok

+

Serve lnbits over https for free using ngrok

+ + + +

How it works

+ +When enabled, ngrok creates a tunnel to ngrok.io with https support and tells you the https web address where you can access your lnbits instance. If you are not the first user to enable it, it doesn't create a new one, it just tells you the existing one. Useful for creating/managing/using lnurls, which must be served either via https or via tor. Note that if you restart your device, your device will generate a new url. If anyone is using your old one for wallets, lnurls, etc., whatever they are doing will stop working. + +

Installation

+ +Check the Extensions page on your instance of lnbits. If you have copy of lnbits with ngrok as one of the built in extensions, click Enable -- that's the only thing you need to do to install it. + +If your copy of lnbits does not have ngrok as one of the built in extensions, stop lnbits, create go into your lnbits folder, and run this command: ./venv/bin/pip install pyngrok. Then go into the lnbits subdirectory and the extensions subdirectory within that. (So lnbits > lnbits > extensions.) Create a new subdirectory in there called freetunnel, download this repository as a zip file, and unzip it in the freetunnel directory. If your unzipper creates a new "freetunnel" subdirectory, take everything out of there and put it in the freetunnel directory you created. Then go back to the top level lnbits directory and run these commands: + +``` +./venv/bin/quart assets +./venv/bin/quart migrate +./venv/bin/hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()' +``` + +

Optional: set up an ngrok.com account

+ +The default setup makes a tunnel on a random subdomain, and the session times out after 24h or a certain bandwith limit. You can set up an account at ngrok.com; a free plan removes the timeout, and a paid plan lets you choose a custom subdomain (or even use your own domain). For this, get an auth token from ngrok.com, and then set it up as `NGROK_AUTHTOKEN` environment variable on your `.env` file e.g., if your auth token is xxxx, add a line NGROK_AUTHTOKEN=xxxx. diff --git a/lnbits/extensions/ngrok/__init__.py b/lnbits/extensions/ngrok/__init__.py new file mode 100644 index 00000000..a60414a3 --- /dev/null +++ b/lnbits/extensions/ngrok/__init__.py @@ -0,0 +1,18 @@ +import asyncio +from fastapi import APIRouter, FastAPI +from fastapi.staticfiles import StaticFiles +from starlette.routing import Mount +from lnbits.db import Database +from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart + +db = Database("ext_ngrok") + +ngrok_ext: APIRouter = APIRouter(prefix="/ngrok", tags=["ngrok"]) + + +def ngrok_renderer(): + return template_renderer(["lnbits/extensions/ngrok/templates"]) + + +from .views import * diff --git a/lnbits/extensions/ngrok/config.json b/lnbits/extensions/ngrok/config.json new file mode 100644 index 00000000..58e9ff8e --- /dev/null +++ b/lnbits/extensions/ngrok/config.json @@ -0,0 +1,6 @@ +{ + "name": "Ngrok", + "short_description": "Serve lnbits over https for free using ngrok", + "icon": "trip_origin", + "contributors": ["supertestnet"] +} diff --git a/lnbits/extensions/ngrok/migrations.py b/lnbits/extensions/ngrok/migrations.py new file mode 100644 index 00000000..f9b8b37d --- /dev/null +++ b/lnbits/extensions/ngrok/migrations.py @@ -0,0 +1,11 @@ +# async def m001_initial(db): + +# await db.execute( +# """ +# CREATE TABLE example.example ( +# id TEXT PRIMARY KEY, +# wallet TEXT NOT NULL, +# time TIMESTAMP NOT NULL DEFAULT """ + db.timestamp_now + """ +# ); +# """ +# ) diff --git a/lnbits/extensions/ngrok/templates/ngrok/index.html b/lnbits/extensions/ngrok/templates/ngrok/index.html new file mode 100644 index 00000000..3af4fa44 --- /dev/null +++ b/lnbits/extensions/ngrok/templates/ngrok/index.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} + +
+
+ + +
+ Access this lnbits instance at the following url +
+ +

{{ ngrok }}

+
+
+
+ +
+ + +
Ngrok extension
+
+ + + +

+ Note that if you restart your device, your device will generate a + new url. If anyone is using your old one for wallets, lnurls, + etc., whatever they are doing will stop working. +

+ Created by + Supertestnet. +
+
+
+
+
+
+ +{% endblock %}{% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/ngrok/views.py b/lnbits/extensions/ngrok/views.py new file mode 100644 index 00000000..7efb23b3 --- /dev/null +++ b/lnbits/extensions/ngrok/views.py @@ -0,0 +1,43 @@ +from http import HTTPStatus + +from lnbits.decorators import check_user_exists + +from . import ngrok_ext, ngrok_renderer +from fastapi import FastAPI, Request +from fastapi.params import Depends +from fastapi.templating import Jinja2Templates + +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse +from lnbits.core.models import User +from os import getenv +from pyngrok import conf, ngrok + +templates = Jinja2Templates(directory="templates") + + +def log_event_callback(log): + string = str(log) + string2 = string[string.find('url="https') : string.find('url="https') + 40] + if string2: + string3 = string2 + string4 = string3[4:] + global string5 + string5 = string4.replace('"', "") + + +conf.get_default().log_event_callback = log_event_callback + +ngrok_authtoken = getenv("NGROK_AUTHTOKEN") +if ngrok_authtoken is not None: + ngrok.set_auth_token(ngrok_authtoken) + +port = getenv("PORT") +ngrok_tunnel = ngrok.connect(port) + + +@ngrok_ext.get("/") +async def index(request: Request, user: User = Depends(check_user_exists)): + return ngrok_renderer().TemplateResponse( + "ngrok/index.html", {"request": request, "user": user.dict()} + ) From aaaa4ce8e7152d6c573b307724effdb52a51e6a2 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Tue, 19 Oct 2021 21:13:26 +0100 Subject: [PATCH 69/75] Added ngrok for easy LNURL developing --- lnbits/extensions/ngrok/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/ngrok/views.py b/lnbits/extensions/ngrok/views.py index 7efb23b3..8b3fe0dd 100644 --- a/lnbits/extensions/ngrok/views.py +++ b/lnbits/extensions/ngrok/views.py @@ -18,7 +18,8 @@ templates = Jinja2Templates(directory="templates") def log_event_callback(log): string = str(log) - string2 = string[string.find('url="https') : string.find('url="https') + 40] + print(string) + string2 = string[string.find('url="https') : string.find('url="https') + 80] if string2: string3 = string2 string4 = string3[4:] @@ -39,5 +40,5 @@ ngrok_tunnel = ngrok.connect(port) @ngrok_ext.get("/") async def index(request: Request, user: User = Depends(check_user_exists)): return ngrok_renderer().TemplateResponse( - "ngrok/index.html", {"request": request, "user": user.dict()} + "ngrok/index.html", {"request": request, "ngrok": string5, "user": user.dict()} ) From 47b3e537f4ec51be0520095a320857d5f5697291 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Tue, 19 Oct 2021 23:05:29 +0100 Subject: [PATCH 70/75] satsdice lnurl pain --- lnbits/extensions/satsdice/lnurl.py | 80 +++++++++++++++++----------- lnbits/extensions/satsdice/models.py | 33 +++++------- lnbits/extensions/satsdice/views.py | 2 +- 3 files changed, 63 insertions(+), 52 deletions(-) diff --git a/lnbits/extensions/satsdice/lnurl.py b/lnbits/extensions/satsdice/lnurl.py index f2b4cf98..a77b166d 100644 --- a/lnbits/extensions/satsdice/lnurl.py +++ b/lnbits/extensions/satsdice/lnurl.py @@ -1,6 +1,7 @@ import shortuuid # type: ignore import hashlib import math +import json from http import HTTPStatus from datetime import datetime from lnbits.core.services import pay_invoice, create_invoice @@ -23,7 +24,8 @@ from lnurl import ( LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse, -) # type: ignore +) +from .models import CreateSatsDicePayment ##############LNURLP STUFF @@ -32,25 +34,32 @@ from lnurl import ( @satsdice_ext.get("/api/v1/lnurlp/{link_id}", name="satsdice.lnurlp_response") async def api_lnurlp_response(req: Request, link_id: str = Query(None)): link = await get_satsdice_pay(link_id) + print(link) if not link: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="LNURL-pay not found." ) - resp = LnurlPayResponse( - callback=req.url_for( - "satsdice.api_lnurlp_callback", link_id=link.id, _external=True - ), - min_sendable=math.ceil(link.min_bet * 1) * 1000, - max_sendable=round(link.max_bet * 1) * 1000, - metadata=link.lnurlpay_metadata, - ) - params = resp.dict() - - return params + payResponse = { + "tag": "payRequest", + "callback": req.url_for("satsdice.api_lnurlp_callback", link_id=link.id), + "metadata": link.lnurlpay_metadata, + "minSendable": math.ceil(link.min_bet * 1) * 1000, + "maxSendable": round(link.max_bet * 1) * 1000, + } + return json.dumps(payResponse) -@satsdice_ext.get("/api/v1/lnurlp/cb/{link_id}") -async def api_lnurlp_callback(link_id: str = Query(None), amount: str = Query(None)): +@satsdice_ext.get( + "/api/v1/lnurlp/cb/{link_id}", + response_class=HTMLResponse, + name="satsdice.api_lnurlp_callback", +) +async def api_lnurlp_callback( + data: CreateSatsDicePayment, + req: Request, + link_id: str = Query(None), + amount: str = Query(None), +): link = await get_satsdice_pay(link_id) if not link: raise HTTPException( @@ -63,13 +72,15 @@ async def api_lnurlp_callback(link_id: str = Query(None), amount: str = Query(No amount_received = int(amount or 0) if amount_received < min: - return LnurlErrorResponse( - reason=f"Amount {amount_received} is smaller than minimum {min}." - ).dict() + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail=f"Amount {amount_received} is smaller than minimum {min}.", + ) elif amount_received > max: - return LnurlErrorResponse( - reason=f"Amount {amount_received} is greater than maximum {max}." - ).dict() + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail=f"Amount {amount_received} is greater than maximum {max}.", + ) payment_hash, payment_request = await create_invoice( wallet_id=link.wallet, @@ -81,20 +92,27 @@ async def api_lnurlp_callback(link_id: str = Query(None), amount: str = Query(No extra={"tag": "satsdice", "link": link.id, "comment": "comment"}, ) - success_action = link.success_action(payment_hash) - data = [] - data.satsdice_payy = link.id + success_action = link.success_action(payment_hash=payment_hash, req=req) + print("success_action") + print(success_action) + print("success_action") + data.satsdice_pay = link.id data.value = amount_received / 1000 data.payment_hash = payment_hash link = await create_satsdice_payment(data) if success_action: - resp = LnurlPayActionResponse( - pr=payment_request, success_action=success_action, routes=[] - ) + payResponse = { + "pr": payment_request, + "success_action": success_action, + "routes": [], + } else: - resp = LnurlPayActionResponse(pr=payment_request, routes=[]) + payResponse = { + "pr": payment_request, + "routes": [], + } - return resp.dict() + return json.dumps(payResponse) ##############LNURLW STUFF @@ -112,13 +130,15 @@ async def api_lnurlw_response(unique_hash: str = Query(None)): if link.used: raise HTTPException(status_code=HTTPStatus.OK, detail="satsdice is spent.") - return link.lnurl_response.dict() + return json.dumps(link.lnurl_response) # CALLBACK -@satsdice_ext.get("/api/v1/lnurlw/cb/{unique_hash}") +@satsdice_ext.get( + "/api/v1/lnurlw/cb/{unique_hash}", name="satsdice.api_lnurlw_callback" +) async def api_lnurlw_callback( unique_hash: str = Query(None), k1: str = Query(None), pr: str = Query(None) ): diff --git a/lnbits/extensions/satsdice/models.py b/lnbits/extensions/satsdice/models.py index 21a7a12a..59324d6f 100644 --- a/lnbits/extensions/satsdice/models.py +++ b/lnbits/extensions/satsdice/models.py @@ -41,16 +41,9 @@ class satsdiceLink(BaseModel): def success_action(self, payment_hash: str, req: Request) -> Optional[Dict]: url = req.url_for( - "satsdice.displaywin", - link_id=self.id, - payment_hash=payment_hash, - _external=True, + "satsdice.displaywin", link_id=self.id, payment_hash=payment_hash ) - # url: ParseResult = urlparse(url) print(url) - # qs: Dict = parse_qs(url.query) - # qs["payment_hash"] = payment_hash - # url = url._replace(query=urlencode(qs, doseq=True)) return {"tag": "url", "description": "Check the attached link", "url": url} @@ -73,9 +66,7 @@ class satsdiceWithdraw(BaseModel): def lnurl(self, req: Request) -> Lnurl: return lnurl_encode( - req.url_for( - "satsdice.lnurlw_response", unique_hash=self.unique_hash, _external=True - ) + req.url_for("satsdice.lnurlw_response", unique_hash=self.unique_hash) ) @property @@ -84,16 +75,16 @@ class satsdiceWithdraw(BaseModel): @property def lnurl_response(self, req: Request) -> LnurlWithdrawResponse: - url = req.url_for( - "satsdice.api_lnurlw_callback", unique_hash=self.unique_hash, _external=True - ) - return LnurlWithdrawResponse( - callback=url, - k1=self.k1, - minWithdrawable=self.value * 1000, - maxWithdrawable=self.value * 1000, - default_description="Satsdice winnings!", - ) + url = req.url_for("satsdice.api_lnurlw_callback", unique_hash=self.unique_hash) + withdrawResponse = { + "tag": "withdrawRequest", + "callback": url, + "k1": self.k1, + "minWithdrawable": self.value * 1000, + "maxWithdrawable": self.value * 1000, + "defaultDescription": "Satsdice winnings!", + } + return withdrawResponse class HashCheck(BaseModel): diff --git a/lnbits/extensions/satsdice/views.py b/lnbits/extensions/satsdice/views.py index 2f2f74bc..ff254b3e 100644 --- a/lnbits/extensions/satsdice/views.py +++ b/lnbits/extensions/satsdice/views.py @@ -50,7 +50,7 @@ async def display(link_id): ) -@satsdice_ext.get("/win/{link_id}/{payment_hash}") +@satsdice_ext.get("/win/{link_id}/{payment_hash}", name="satsdice.displaywin") async def displaywin(link_id: str = Query(None), payment_hash: str = Query(None)): satsdicelink = await get_satsdice_pay(link_id) or abort( HTTPStatus.NOT_FOUND, "satsdice link does not exist." From e0db0bc6cd381eb95b0ce98b9c83cfa6534a17a0 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Wed, 20 Oct 2021 03:08:43 +0100 Subject: [PATCH 71/75] Satsdice working, although invoices are not being seen as paid --- lnbits/extensions/satsdice/crud.py | 30 ++++---- lnbits/extensions/satsdice/lnurl.py | 71 +++++++++++------- lnbits/extensions/satsdice/models.py | 3 +- lnbits/extensions/satsdice/views.py | 107 ++++++++++++++++++--------- 4 files changed, 129 insertions(+), 82 deletions(-) diff --git a/lnbits/extensions/satsdice/crud.py b/lnbits/extensions/satsdice/crud.py index dd7d484a..4fd3c8c2 100644 --- a/lnbits/extensions/satsdice/crud.py +++ b/lnbits/extensions/satsdice/crud.py @@ -60,7 +60,7 @@ async def get_satsdice_pay(link_id: str) -> Optional[satsdiceLink]: row = await db.fetchone( "SELECT * FROM satsdice.satsdice_pay WHERE id = ?", (link_id,) ) - return satsdiceLink.from_row(row) if row else None + return satsdiceLink(**row) if row else None async def get_satsdice_pays(wallet_ids: Union[str, List[str]]) -> List[satsdiceLink]: @@ -102,7 +102,7 @@ async def increment_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLin row = await db.fetchone( "SELECT * FROM satsdice.satsdice_pay WHERE id = ?", (link_id,) ) - return satsdiceLink.from_row(row) if row else None + return satsdiceLink(**row) if row else None async def delete_satsdice_pay(link_id: int) -> None: @@ -124,9 +124,9 @@ async def create_satsdice_payment(data: CreateSatsDicePayment) -> satsdicePaymen ) VALUES (?, ?, ?, ?, ?) """, - (data.payment_hash, data.satsdice_pay, data.value, False, False), + (data["payment_hash"], data["satsdice_pay"], data["value"], False, False), ) - payment = await get_satsdice_payment(payment_hash) + payment = await get_satsdice_payment(data["payment_hash"]) assert payment, "Newly created withdraw couldn't be retrieved" return payment @@ -136,7 +136,7 @@ async def get_satsdice_payment(payment_hash: str) -> Optional[satsdicePayment]: "SELECT * FROM satsdice.satsdice_payment WHERE payment_hash = ?", (payment_hash,), ) - return satsdicePayment.from_row(row) if row else None + return satsdicePayment(**row) if row else None async def update_satsdice_payment( @@ -152,7 +152,7 @@ async def update_satsdice_payment( "SELECT * FROM satsdice.satsdice_payment WHERE payment_hash = ?", (payment_hash,), ) - return satsdicePayment.from_row(row) if row else None + return satsdicePayment(**row) if row else None ##################SATSDICE WITHDRAW LINKS @@ -173,16 +173,16 @@ async def create_satsdice_withdraw(data: CreateSatsDiceWithdraw) -> satsdiceWith VALUES (?, ?, ?, ?, ?, ?, ?) """, ( - data.payment_hash, - data.satsdice_pay, - data.value, + data["payment_hash"], + data["satsdice_pay"], + data["value"], urlsafe_short_hash(), urlsafe_short_hash(), int(datetime.now().timestamp()), - data.used, + data["used"], ), ) - withdraw = await get_satsdice_withdraw(payment_hash, 0) + withdraw = await get_satsdice_withdraw(data["payment_hash"], 0) assert withdraw, "Newly created withdraw couldn't be retrieved" return withdraw @@ -198,7 +198,7 @@ async def get_satsdice_withdraw(withdraw_id: str, num=0) -> Optional[satsdiceWit for item in row: withdraw.append(item) withdraw.append(num) - return satsdiceWithdraw.from_row(row) + return satsdiceWithdraw(**row) async def get_satsdice_withdraw_by_hash( @@ -214,7 +214,7 @@ async def get_satsdice_withdraw_by_hash( for item in row: withdraw.append(item) withdraw.append(num) - return satsdiceWithdraw.from_row(row) + return satsdiceWithdraw(**row) async def get_satsdice_withdraws( @@ -229,7 +229,7 @@ async def get_satsdice_withdraws( (*wallet_ids,), ) - return [satsdiceWithdraw.from_row(row) for row in rows] + return [satsdiceWithdraw(**row) for row in rows] async def update_satsdice_withdraw( @@ -243,7 +243,7 @@ async def update_satsdice_withdraw( row = await db.fetchone( "SELECT * FROM satsdice.satsdice_withdraw WHERE id = ?", (withdraw_id,) ) - return satsdiceWithdraw.from_row(row) if row else None + return satsdiceWithdraw(**row) if row else None async def delete_satsdice_withdraw(withdraw_id: str) -> None: diff --git a/lnbits/extensions/satsdice/lnurl.py b/lnbits/extensions/satsdice/lnurl.py index a77b166d..1d46f054 100644 --- a/lnbits/extensions/satsdice/lnurl.py +++ b/lnbits/extensions/satsdice/lnurl.py @@ -31,10 +31,13 @@ from .models import CreateSatsDicePayment ##############LNURLP STUFF -@satsdice_ext.get("/api/v1/lnurlp/{link_id}", name="satsdice.lnurlp_response") +@satsdice_ext.get( + "/api/v1/lnurlp/{link_id}", + response_class=HTMLResponse, + name="satsdice.lnurlp_response", +) async def api_lnurlp_response(req: Request, link_id: str = Query(None)): link = await get_satsdice_pay(link_id) - print(link) if not link: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="LNURL-pay not found." @@ -55,12 +58,12 @@ async def api_lnurlp_response(req: Request, link_id: str = Query(None)): name="satsdice.api_lnurlp_callback", ) async def api_lnurlp_callback( - data: CreateSatsDicePayment, req: Request, link_id: str = Query(None), amount: str = Query(None), ): link = await get_satsdice_pay(link_id) + print(link) if not link: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="LNURL-pay not found." @@ -93,24 +96,20 @@ async def api_lnurlp_callback( ) success_action = link.success_action(payment_hash=payment_hash, req=req) - print("success_action") - print(success_action) - print("success_action") - data.satsdice_pay = link.id - data.value = amount_received / 1000 - data.payment_hash = payment_hash - link = await create_satsdice_payment(data) - if success_action: - payResponse = { - "pr": payment_request, - "success_action": success_action, - "routes": [], - } - else: - payResponse = { - "pr": payment_request, - "routes": [], - } + + data: CreateSatsDicePayment = { + "satsdice_pay": link.id, + "value": amount_received / 1000, + "payment_hash": payment_hash, + } + + await create_satsdice_payment(data) + payResponse = { + "pr": payment_request, + "successAction": success_action, + "routes": [], + } + print(json.dumps(payResponse)) return json.dumps(payResponse) @@ -118,29 +117,45 @@ async def api_lnurlp_callback( ##############LNURLW STUFF -@satsdice_ext.get("/api/v1/lnurlw/{unique_hash}", name="satsdice.lnurlw_response") -async def api_lnurlw_response(unique_hash: str = Query(None)): +@satsdice_ext.get( + "/api/v1/lnurlw/{unique_hash}", + response_class=HTMLResponse, + name="satsdice.lnurlw_response", +) +async def api_lnurlw_response(req: Request, unique_hash: str = Query(None)): link = await get_satsdice_withdraw_by_hash(unique_hash) if not link: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="LNURL-satsdice not found." ) - if link.used: raise HTTPException(status_code=HTTPStatus.OK, detail="satsdice is spent.") - - return json.dumps(link.lnurl_response) + url = req.url_for("satsdice.api_lnurlw_callback", unique_hash=link.unique_hash) + withdrawResponse = { + "tag": "withdrawRequest", + "callback": url, + "k1": link.k1, + "minWithdrawable": link.value * 1000, + "maxWithdrawable": link.value * 1000, + "defaultDescription": "Satsdice winnings!", + } + return json.dumps(withdrawResponse) # CALLBACK @satsdice_ext.get( - "/api/v1/lnurlw/cb/{unique_hash}", name="satsdice.api_lnurlw_callback" + "/api/v1/lnurlw/cb/{unique_hash}", + response_class=HTMLResponse, + name="satsdice.api_lnurlw_callback", ) async def api_lnurlw_callback( - unique_hash: str = Query(None), k1: str = Query(None), pr: str = Query(None) + req: Request, + unique_hash: str = Query(None), + k1: str = Query(None), + pr: str = Query(None), ): link = await get_satsdice_withdraw_by_hash(unique_hash) paylink = await get_satsdice_pay(link.satsdice_pay) diff --git a/lnbits/extensions/satsdice/models.py b/lnbits/extensions/satsdice/models.py index 59324d6f..3ed5b99e 100644 --- a/lnbits/extensions/satsdice/models.py +++ b/lnbits/extensions/satsdice/models.py @@ -43,7 +43,6 @@ class satsdiceLink(BaseModel): url = req.url_for( "satsdice.displaywin", link_id=self.id, payment_hash=payment_hash ) - print(url) return {"tag": "url", "description": "Check the attached link", "url": url} @@ -102,7 +101,7 @@ class CreateSatsDiceLink(BaseModel): base_url: str = Query(None) min_bet: str = Query(None) max_bet: str = Query(None) - multiplier: int = Query(0) + multiplier: float = Query(0) chance: float = Query(0) haircut: int = Query(0) diff --git a/lnbits/extensions/satsdice/views.py b/lnbits/extensions/satsdice/views.py index ff254b3e..2c16d4ab 100644 --- a/lnbits/extensions/satsdice/views.py +++ b/lnbits/extensions/satsdice/views.py @@ -22,9 +22,10 @@ from fastapi.templating import Jinja2Templates from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse from lnbits.core.models import User, Payment - from fastapi.params import Depends from fastapi.param_functions import Query +import random +from .models import CreateSatsDiceWithdraw templates = Jinja2Templates(directory="templates") @@ -37,35 +38,47 @@ async def index(request: Request, user: User = Depends(check_user_exists)): @satsdice_ext.get("/{link_id}") -async def display(link_id): +async def display(request: Request, link_id: str = Query(None)): link = await get_satsdice_pay(link_id) or abort( HTTPStatus.NOT_FOUND, "satsdice link does not exist." ) return satsdice_renderer().TemplateResponse( "satsdice/display.html", - chance=link.chance, - multiplier=link.multiplier, - lnurl=link.lnurl, - unique=True, + { + "request": request, + "chance": link.chance, + "multiplier": link.multiplier, + "lnurl": link.lnurl(request), + "unique": True, + }, ) @satsdice_ext.get("/win/{link_id}/{payment_hash}", name="satsdice.displaywin") -async def displaywin(link_id: str = Query(None), payment_hash: str = Query(None)): +async def displaywin( + request: Request, link_id: str = Query(None), payment_hash: str = Query(None) +): satsdicelink = await get_satsdice_pay(link_id) or abort( HTTPStatus.NOT_FOUND, "satsdice link does not exist." ) - withdrawLink = await get_satsdice_withdraw(payment_hash) + status = await check_invoice_status( + wallet_id=satsdicelink.wallet, payment_hash=payment_hash + ) + + withdrawLink = await get_satsdice_withdraw(payment_hash) if withdrawLink: return satsdice_renderer().TemplateResponse( "satsdice/displaywin.html", - value=withdrawLink.value, - chance=satsdicelink.chance, - multiplier=satsdicelink.multiplier, - lnurl=withdrawLink.lnurl, - paid=False, - lost=False, + { + "request": request, + "value": withdrawLink.value, + "chance": satsdicelink.chance, + "multiplier": satsdicelink.multiplier, + "lnurl": withdrawLink.lnurl(request), + "paid": False, + "lost": False, + }, ) payment = await get_standalone_payment(payment_hash) or abort( @@ -78,43 +91,63 @@ async def displaywin(link_id: str = Query(None), payment_hash: str = Query(None) HTTPStatus.NOT_FOUND, "satsdice link does not exist." ) if payment.pending == 1: - print("pending") + print("cunt") return satsdice_renderer().TemplateResponse( - "satsdice/error.html", link=satsdicelink.id, paid=False, lost=False + "satsdice/error.html", + { + "request": request, + "link": satsdicelink.id, + "paid": False, + "lost": False, + }, ) await update_satsdice_payment(payment_hash, paid=1) + paylink = await get_satsdice_payment(payment_hash) + if not paylink: - paylink = await get_satsdice_payment(payment_hash) or abort( - HTTPStatus.NOT_FOUND, "satsdice link does not exist." - ) - - if paylink.lost == 1: - print("lost") - return satsdice_renderer().TemplateResponse( - "satsdice/error.html", link=satsdicelink.id, paid=False, lost=True - ) + return satsdice_renderer().TemplateResponse( + "satsdice/error.html", + { + "request": request, + "link": satsdicelink.id, + "paid": False, + "lost": True, + }, + ) rand = random.randint(0, 100) chance = satsdicelink.chance if rand > chance: await update_satsdice_payment(payment_hash, lost=1) return satsdice_renderer().TemplateResponse( - "satsdice/error.html", link=satsdicelink.id, paid=False, lost=True + "satsdice/error.html", + { + "request": request, + "link": satsdicelink.id, + "paid": False, + "lost": True, + }, ) - data = [] - data.payment_hash = payment_hash - data.satsdice_pay = (satsdicelink.id,) - data.value = (paylink.value * satsdicelink.multiplier,) - data.used = 0 + + data: CreateSatsDiceWithdraw = { + "satsdice_pay": satsdicelink.id, + "value": paylink.value * satsdicelink.multiplier, + "payment_hash": payment_hash, + "used": 0, + } + withdrawLink = await create_satsdice_withdraw(data) return satsdice_renderer().TemplateResponse( "satsdice/displaywin.html", - value=withdrawLink.value, - chance=satsdicelink.chance, - multiplier=satsdicelink.multiplier, - lnurl=withdrawLink.lnurl, - paid=False, - lost=False, + { + "request": request, + "value": withdrawLink.value, + "chance": satsdicelink.chance, + "multiplier": satsdicelink.multiplier, + "lnurl": withdrawLink.lnurl(request), + "paid": False, + "lost": False, + }, ) From b6f5052da0ad9fb137d42d4c193f12970b2cd16d Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Wed, 20 Oct 2021 03:28:31 +0100 Subject: [PATCH 72/75] trying to fix payment check --- lnbits/core/services.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lnbits/core/services.py b/lnbits/core/services.py index a7577217..02f14fdc 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -306,13 +306,26 @@ async def perform_lnurlauth( async def check_invoice_status( - wallet_id: str, payment_hash: str, conn: Optional[Connection] = None + wallet_id: str, + payment_hash: str, + conn: Optional[Connection] = None, ) -> PaymentStatus: payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn) if not payment: return PaymentStatus(None) - - return await WALLET.get_invoice_status(payment.checking_id) + status = await WALLET.get_invoice_status(payment.checking_id) + print(status) + if not payment.pending: + return status + if payment.is_out and status.failed: + print(f" - deleting outgoing failed payment {payment.checking_id}: {status}") + await payment.delete() + elif not status.pending: + print( + f" - marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}" + ) + await payment.set_pending(status.pending) + return status def fee_reserve(amount_msat: int) -> int: From 19df2f888b90ab415d7b2ff7cfa19ce2c36df235 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Wed, 20 Oct 2021 03:59:24 +0100 Subject: [PATCH 73/75] Added decode lnurl and extra payment check --- lnbits/core/templates/core/_api_docs.html | 31 ++++++++++++++++++- lnbits/core/views/api.py | 37 ++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/lnbits/core/templates/core/_api_docs.html b/lnbits/core/templates/core/_api_docs.html index 4bb38fad..2acadc76 100644 --- a/lnbits/core/templates/core/_api_docs.html +++ b/lnbits/core/templates/core/_api_docs.html @@ -19,7 +19,7 @@ GET /api/v1/wallet
Headers
- {"X-Api-Key": "{{ wallet.adminkey }}"}
+ {"X-Api-Key": "{{ wallet.inkey }}"}
Returns 200 OK (application/json)
@@ -94,6 +94,35 @@
+ + + + + POST + /api/v1/payments/decode +
Headers
+ {"X-Api-Key": "{{ wallet.inkey }}"}
+
Body (application/json)
+ {"invoice": <string>} +
+ Returns 200 (application/json) +
+
Curl example
+ curl -X POST {{ request.url_root }}api/v1/payments/decode -d + '{"data": <bolt11/lnurl, string>}' -H "X-Api-Key: + {{ wallet.inkey }}" -H "Content-type: application/json" +
+
+
Date: Wed, 20 Oct 2021 04:52:33 +0100 Subject: [PATCH 74/75] Satsdice working --- lnbits/extensions/satsdice/models.py | 11 ++++++++++- lnbits/extensions/satsdice/views.py | 10 +++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lnbits/extensions/satsdice/models.py b/lnbits/extensions/satsdice/models.py index 3ed5b99e..11c8da69 100644 --- a/lnbits/extensions/satsdice/models.py +++ b/lnbits/extensions/satsdice/models.py @@ -37,7 +37,16 @@ class satsdiceLink(BaseModel): @property def lnurlpay_metadata(self) -> LnurlPayMetadata: - return LnurlPayMetadata(json.dumps([["text/plain", self.title]])) + return LnurlPayMetadata( + json.dumps( + [ + [ + "text/plain", + f"{self.title} (Chance: {self.chance}%, Multiplier: {self.multiplier})", + ] + ] + ) + ) def success_action(self, payment_hash: str, req: Request) -> Optional[Dict]: url = req.url_for( diff --git a/lnbits/extensions/satsdice/views.py b/lnbits/extensions/satsdice/views.py index 2c16d4ab..53c1b420 100644 --- a/lnbits/extensions/satsdice/views.py +++ b/lnbits/extensions/satsdice/views.py @@ -91,14 +91,13 @@ async def displaywin( HTTPStatus.NOT_FOUND, "satsdice link does not exist." ) if payment.pending == 1: - print("cunt") return satsdice_renderer().TemplateResponse( "satsdice/error.html", { "request": request, "link": satsdicelink.id, "paid": False, - "lost": False, + "lost": True, }, ) @@ -115,7 +114,12 @@ async def displaywin( "lost": True, }, ) - rand = random.randint(0, 100) + rand1 = random.randint(0, 100) + rand2 = random.randint(0, 100) + rand3 = random.randint(0, 100) + rand4 = random.randint(0, 100) + rand = (rand1 + rand2 + rand3 + rand4) / 4 + print(rand) chance = satsdicelink.chance if rand > chance: await update_satsdice_payment(payment_hash, lost=1) From 552fa8edc64660381f41e60b6a2e67d754161ca8 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Wed, 20 Oct 2021 06:49:59 +0100 Subject: [PATCH 75/75] LNURLPoS loading, form working --- lnbits/extensions/lnurlpos/README.md | 3 + lnbits/extensions/lnurlpos/__init__.py | 20 + lnbits/extensions/lnurlpos/config.json | 6 + lnbits/extensions/lnurlpos/crud.py | 113 +++++ lnbits/extensions/lnurlpos/lnurl.py | 108 ++++ lnbits/extensions/lnurlpos/migrations.py | 30 ++ lnbits/extensions/lnurlpos/models.py | 65 +++ .../templates/lnurlpos/_api_docs.html | 158 ++++++ .../lnurlpos/templates/lnurlpos/error.html | 34 ++ .../lnurlpos/templates/lnurlpos/index.html | 472 ++++++++++++++++++ .../lnurlpos/templates/lnurlpos/paid.html | 27 + lnbits/extensions/lnurlpos/views.py | 60 +++ lnbits/extensions/lnurlpos/views_api.py | 94 ++++ 13 files changed, 1190 insertions(+) create mode 100644 lnbits/extensions/lnurlpos/README.md create mode 100644 lnbits/extensions/lnurlpos/__init__.py create mode 100644 lnbits/extensions/lnurlpos/config.json create mode 100644 lnbits/extensions/lnurlpos/crud.py create mode 100644 lnbits/extensions/lnurlpos/lnurl.py create mode 100644 lnbits/extensions/lnurlpos/migrations.py create mode 100644 lnbits/extensions/lnurlpos/models.py create mode 100644 lnbits/extensions/lnurlpos/templates/lnurlpos/_api_docs.html create mode 100644 lnbits/extensions/lnurlpos/templates/lnurlpos/error.html create mode 100644 lnbits/extensions/lnurlpos/templates/lnurlpos/index.html create mode 100644 lnbits/extensions/lnurlpos/templates/lnurlpos/paid.html create mode 100644 lnbits/extensions/lnurlpos/views.py create mode 100644 lnbits/extensions/lnurlpos/views_api.py diff --git a/lnbits/extensions/lnurlpos/README.md b/lnbits/extensions/lnurlpos/README.md new file mode 100644 index 00000000..e7713055 --- /dev/null +++ b/lnbits/extensions/lnurlpos/README.md @@ -0,0 +1,3 @@ +# LNURLPoS + +For offline LNURL PoS devices diff --git a/lnbits/extensions/lnurlpos/__init__.py b/lnbits/extensions/lnurlpos/__init__.py new file mode 100644 index 00000000..4c86c827 --- /dev/null +++ b/lnbits/extensions/lnurlpos/__init__.py @@ -0,0 +1,20 @@ +import asyncio +from fastapi import APIRouter, FastAPI +from fastapi.staticfiles import StaticFiles +from starlette.routing import Mount +from lnbits.db import Database +from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart + +db = Database("ext_lnurlpos") + +lnurlpos_ext: APIRouter = APIRouter(prefix="/lnurlpos", tags=["lnurlpos"]) + + +def lnurlpos_renderer(): + return template_renderer(["lnbits/extensions/lnurlpos/templates"]) + + +from .views_api import * # noqa +from .views import * # noqa +from .lnurl import * # noqa diff --git a/lnbits/extensions/lnurlpos/config.json b/lnbits/extensions/lnurlpos/config.json new file mode 100644 index 00000000..2688e5a5 --- /dev/null +++ b/lnbits/extensions/lnurlpos/config.json @@ -0,0 +1,6 @@ +{ + "name": "LNURLPoS", + "short_description": "For offline LNURL PoS systems", + "icon": "point_of_sale", + "contributors": ["arcbtc"] +} diff --git a/lnbits/extensions/lnurlpos/crud.py b/lnbits/extensions/lnurlpos/crud.py new file mode 100644 index 00000000..5a85fa33 --- /dev/null +++ b/lnbits/extensions/lnurlpos/crud.py @@ -0,0 +1,113 @@ +from datetime import datetime +from typing import List, Optional, Union +from lnbits.helpers import urlsafe_short_hash +from typing import List, Optional +from . import db +from .models import lnurlposs, lnurlpospayment, createLnurlpos + +###############lnurlposS########################## + + +async def create_lnurlpos( + data: createLnurlpos, +) -> lnurlposs: + print(data) + lnurlpos_id = urlsafe_short_hash() + lnurlpos_key = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO lnurlpos.lnurlposs ( + id, + key, + title, + wallet, + currency + ) + VALUES (?, ?, ?, ?, ?) + """, + (lnurlpos_id, lnurlpos_key, data.title, data.wallet, data.currency), + ) + return await get_lnurlpos(lnurlpos_id) + + +async def update_lnurlpos(lnurlpos_id: str, **kwargs) -> Optional[lnurlposs]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE lnurlpos.lnurlposs SET {q} WHERE id = ?", + (*kwargs.values(), lnurlpos_id), + ) + row = await db.fetchone( + "SELECT * FROM lnurlpos.lnurlposs WHERE id = ?", (lnurlpos_id,) + ) + return lnurlposs.from_row(row) if row else None + + +async def get_lnurlpos(lnurlpos_id: str) -> lnurlposs: + row = await db.fetchone( + "SELECT * FROM lnurlpos.lnurlposs WHERE id = ?", (lnurlpos_id,) + ) + return lnurlposs.from_row(row) if row else None + + +async def get_lnurlposs(wallet_ids: Union[str, List[str]]) -> List[lnurlposs]: + wallet_ids = [wallet_ids] + q = ",".join(["?"] * len(wallet_ids[0])) + rows = await db.fetchall( + f""" + SELECT * FROM lnurlpos.lnurlposs WHERE wallet IN ({q}) + ORDER BY id + """, + (*wallet_ids,), + ) + + return [lnurlposs.from_row(row) for row in rows] + + +async def delete_lnurlpos(lnurlpos_id: str) -> None: + await db.execute("DELETE FROM lnurlpos.lnurlposs WHERE id = ?", (lnurlpos_id,)) + + ########################lnulpos payments########################### + + +async def create_lnurlpospayment( + posid: str, + payload: Optional[str] = None, + pin: Optional[str] = None, + sats: Optional[int] = 0, +) -> lnurlpospayment: + lnurlpospayment_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO lnurlpos.lnurlpospayment ( + id, + posid, + payload, + pin, + sats + ) + VALUES (?, ?, ?, ?, ?) + """, + (lnurlpospayment_id, posid, payload, pin, sats), + ) + return await get_lnurlpospayment(lnurlpospayment_id) + + +async def update_lnurlpospayment( + lnurlpospayment_id: str, **kwargs +) -> Optional[lnurlpospayment]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE lnurlpos.lnurlpospayment SET {q} WHERE id = ?", + (*kwargs.values(), lnurlpospayment_id), + ) + row = await db.fetchone( + "SELECT * FROM lnurlpos.lnurlpospayment WHERE id = ?", (lnurlpospayment_id,) + ) + return lnurlpospayment.from_row(row) if row else None + + +async def get_lnurlpospayment(lnurlpospayment_id: str) -> lnurlpospayment: + row = await db.fetchone( + "SELECT * FROM lnurlpos.lnurlpospayment WHERE id = ?", (lnurlpospayment_id,) + ) + return lnurlpospayment.from_row(row) if row else None diff --git a/lnbits/extensions/lnurlpos/lnurl.py b/lnbits/extensions/lnurlpos/lnurl.py new file mode 100644 index 00000000..2cfbc4ba --- /dev/null +++ b/lnbits/extensions/lnurlpos/lnurl.py @@ -0,0 +1,108 @@ +import json +import hashlib +import math +from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore +from lnurl.types import LnurlPayMetadata +from lnbits.core.services import create_invoice +from hashlib import md5 +from fastapi import Request +from fastapi.param_functions import Query +from . import lnurlpos_ext +from fastapi.templating import Jinja2Templates +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse +from http import HTTPStatus +from fastapi.params import Depends +from fastapi.param_functions import Query +from .crud import ( + get_lnurlpos, + create_lnurlpospayment, + get_lnurlpospayment, + update_lnurlpospayment, +) +from lnbits.utils.exchange_rates import fiat_amount_as_satoshis + + +@lnurlpos_ext.get("/api/v1/lnurl/{nonce}/{payload}/{pos_id}") +async def lnurl_response( + request: Request, + nonce: str = Query(None), + pos_id: str = Query(None), + payload: str = Query(None), +): + pos = await get_lnurlpos(pos_id) + if not pos: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="lnurlpos not found." + ) + nonce1 = bytes.fromhex(nonce) + payload1 = bytes.fromhex(payload) + h = hashlib.sha256(nonce1) + h.update(pos.key.encode()) + s = h.digest() + res = bytearray(payload1) + for i in range(len(res)): + res[i] = res[i] ^ s[i] + decryptedAmount = float(int.from_bytes(res[2:6], "little") / 100) + decryptedPin = int.from_bytes(res[:2], "little") + if type(decryptedAmount) != float: + + raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not an amount.") + price_msat = ( + await fiat_amount_as_satoshis(decryptedAmount, pos.currency) + if pos.currency != "sat" + else pos.currency + ) * 1000 + + lnurlpospayment = await create_lnurlpospayment( + posid=pos.id, payload=payload, sats=price_msat, pin=decryptedPin + ) + if not lnurlpospayment: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail="Could not create payment" + ) + + payResponse = { + "tag": "payRequest", + "callback": request.url_for( + "lnurlpos.lnurl_callback", + paymentid=lnurlpospayment.id, + ), + "metadata": LnurlPayMetadata(json.dumps([["text/plain", str(pos.title)]])), + "minSendable": price_msat, + "maxSendable": price_msat, + } + return json.dumps(payResponse) + + +@lnurlpos_ext.get("/api/v1/lnurl/cb/{paymentid}") +async def lnurl_callback(paymentid: str = Query(None)): + lnurlpospayment = await get_lnurlpospayment(paymentid) + pos = await get_lnurlpos(lnurlpospayment.posid) + if not pos: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail="lnurlpos not found." + ) + payment_hash, payment_request = await create_invoice( + wallet_id=pos.wallet, + amount=int(lnurlpospayment.sats / 1000), + memo=pos.title, + description_hash=hashlib.sha256( + (LnurlPayMetadata(json.dumps([["text/plain", str(pos.title)]]))).encode( + "utf-8" + ) + ).digest(), + extra={"tag": "lnurlpos"}, + ) + lnurlpospayment = await update_lnurlpospayment( + lnurlpospayment_id=paymentid, payhash=payment_hash + ) + success_action = pos.success_action(paymentid) + + payResponse = { + "pr": payment_request, + "success_action": success_action, + "disposable": False, + "routes": [], + } + return json.dumps(payResponse) diff --git a/lnbits/extensions/lnurlpos/migrations.py b/lnbits/extensions/lnurlpos/migrations.py new file mode 100644 index 00000000..011cb4a3 --- /dev/null +++ b/lnbits/extensions/lnurlpos/migrations.py @@ -0,0 +1,30 @@ +async def m001_initial(db): + """ + Initial lnurlpos table. + """ + + await db.execute( + f""" + CREATE TABLE lnurlpos.lnurlposs ( + id TEXT NOT NULL PRIMARY KEY, + key TEXT NOT NULL, + title TEXT NOT NULL, + wallet TEXT NOT NULL, + currency TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); + """ + ) + await db.execute( + f""" + CREATE TABLE lnurlpos.lnurlpospayment ( + id TEXT NOT NULL PRIMARY KEY, + posid TEXT NOT NULL, + payhash TEXT, + payload TEXT NOT NULL, + pin INT, + sats INT, + timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); + """ + ) diff --git a/lnbits/extensions/lnurlpos/models.py b/lnbits/extensions/lnurlpos/models.py new file mode 100644 index 00000000..b6924593 --- /dev/null +++ b/lnbits/extensions/lnurlpos/models.py @@ -0,0 +1,65 @@ +import json +from lnurl import Lnurl, LnurlWithdrawResponse, encode as lnurl_encode # type: ignore +from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult +from lnurl.types import LnurlPayMetadata # type: ignore +from sqlite3 import Row +from typing import NamedTuple, Optional, Dict +import shortuuid # type: ignore +from fastapi.param_functions import Query +from pydantic.main import BaseModel +from pydantic import BaseModel +from typing import Optional +from fastapi import FastAPI, Request + + +class createLnurlpos(BaseModel): + title: str + wallet: str + currency: str + + +class lnurlposs(BaseModel): + id: str + key: str + title: str + wallet: str + currency: str + timestamp: str + + @classmethod + def from_row(cls, row: Row) -> "lnurlposs": + return cls(**dict(row)) + + @property + def lnurl(self) -> Lnurl: + url = url_for("lnurlpos.lnurl_response", pos_id=self.id, _external=True) + return lnurl_encode(url) + + @property + def lnurlpay_metadata(self) -> LnurlPayMetadata: + return LnurlPayMetadata(json.dumps([["text/plain", self.title]])) + + def success_action(self, paymentid: str, req: Request) -> Optional[Dict]: + url = url_for( + "lnurlpos.displaypin", + paymentid=paymentid, + ) + return { + "tag": "url", + "description": "Check the attached link", + "url": url, + } + + +class lnurlpospayment(BaseModel): + id: str + posid: str + payhash: str + payload: str + pin: int + sats: int + timestamp: str + + @classmethod + def from_row(cls, row: Row) -> "lnurlpospayment": + return cls(**dict(row)) diff --git a/lnbits/extensions/lnurlpos/templates/lnurlpos/_api_docs.html b/lnbits/extensions/lnurlpos/templates/lnurlpos/_api_docs.html new file mode 100644 index 00000000..071d6d6c --- /dev/null +++ b/lnbits/extensions/lnurlpos/templates/lnurlpos/_api_docs.html @@ -0,0 +1,158 @@ + + +

+ Register LNURLPoS devices to recieve payments in your LNbits wallet.
+ Build your own here + https://github.com/arcbtc/LNURLPoS
+ + Created by, Ben Arc +

+
+ + + + + POST /lnurlpos/api/v1/lnurlpos +
Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<lnurlpos_object>, ...] +
Curl example
+ curl -X POST {{ request.url_root }}api/v1/lnurlpos -d '{"title": + <string>, "message":<string>, "currency": + <integer>}' -H "Content-type: application/json" -H "X-Api-Key: + {{user.wallets[0].adminkey }}" + +
+
+
+ + + + PUT + /lnurlpos/api/v1/lnurlpos/<lnurlpos_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<lnurlpos_object>, ...] +
Curl example
+ curl -X POST {{ request.url_root + }}api/v1/lnurlpos/<lnurlpos_id> -d ''{"title": <string>, + "message":<string>, "currency": <integer>} -H + "Content-type: application/json" -H "X-Api-Key: + {{user.wallets[0].adminkey }}" + +
+
+
+ + + + + GET + /lnurlpos/api/v1/lnurlpos/<lnurlpos_id> +
Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<lnurlpos_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root + }}api/v1/lnurlpos/<lnurlpos_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + GET /lnurlpos/api/v1/lnurlposs +
Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<lnurlpos_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/lnurlposs -H "X-Api-Key: + {{ user.wallets[0].inkey }}" + +
+
+
+ + + + DELETE + /lnurlpos/api/v1/lnurlpos/<lnurlpos_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Returns 204 NO CONTENT
+ +
Curl example
+ curl -X DELETE {{ request.url_root + }}api/v1/lnurlpos/<lnurlpos_id> -H "X-Api-Key: {{ + user.wallets[0].adminkey }}" + +
+
+
+
+
diff --git a/lnbits/extensions/lnurlpos/templates/lnurlpos/error.html b/lnbits/extensions/lnurlpos/templates/lnurlpos/error.html new file mode 100644 index 00000000..d8e41832 --- /dev/null +++ b/lnbits/extensions/lnurlpos/templates/lnurlpos/error.html @@ -0,0 +1,34 @@ +{% extends "public.html" %} {% block page %} +
+
+ + +
+

LNURL-pay not paid

+
+ + +
+
+
+
+
+ + {% endblock %} {% block scripts %} + + + + {% endblock %} +
diff --git a/lnbits/extensions/lnurlpos/templates/lnurlpos/index.html b/lnbits/extensions/lnurlpos/templates/lnurlpos/index.html new file mode 100644 index 00000000..e6d8cd8f --- /dev/null +++ b/lnbits/extensions/lnurlpos/templates/lnurlpos/index.html @@ -0,0 +1,472 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + {% raw %} + New LNURLPoS instance + + + + + + +
+
+
lNURLPoS
+
+ +
+ + + + Export to CSV +
+
+ + + + + {% endraw %} + +
+
+
+ +
+ + +
+ {{SITE_TITLE}} LNURLPoS Extension +
+
+ + + {% include "lnurlpos/_api_docs.html" %} + +
+
+ + + +
Copy to LNURLPoS device
+
+ {% raw %} String server = "{{location}}";
+ String posId = "{{settingsDialog.data.id}}";
+ String key = "{{settingsDialog.data.key}}";
+ String currency = "{{settingsDialog.data.currency}}";{% endraw %} +
+
+
+ + + + + + + + + + +
+ Update lnurlpos + Create lnurlpos + Cancel +
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + + +{% endblock %} diff --git a/lnbits/extensions/lnurlpos/templates/lnurlpos/paid.html b/lnbits/extensions/lnurlpos/templates/lnurlpos/paid.html new file mode 100644 index 00000000..c185ecce --- /dev/null +++ b/lnbits/extensions/lnurlpos/templates/lnurlpos/paid.html @@ -0,0 +1,27 @@ +{% extends "public.html" %} {% block page %} +
+
+ + +
+

{{ pin }}

+
+
+
+
+
+ + {% endblock %} {% block scripts %} + + + + {% endblock %} +
diff --git a/lnbits/extensions/lnurlpos/views.py b/lnbits/extensions/lnurlpos/views.py new file mode 100644 index 00000000..68e4ef06 --- /dev/null +++ b/lnbits/extensions/lnurlpos/views.py @@ -0,0 +1,60 @@ +from http import HTTPStatus +import httpx +from collections import defaultdict +from lnbits.decorators import check_user_exists + +from .crud import get_lnurlpos, get_lnurlpospayment +from functools import wraps +from lnbits.core.crud import get_standalone_payment +import hashlib +from lnbits.core.services import check_invoice_status +from lnbits.core.crud import update_payment_status +from fastapi import FastAPI, Request +from fastapi.templating import Jinja2Templates +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse +from fastapi.params import Depends +from fastapi.param_functions import Query +import random + +from datetime import datetime +from http import HTTPStatus +from . import lnurlpos_ext, lnurlpos_renderer +from lnbits.core.models import User, Payment + +templates = Jinja2Templates(directory="templates") + + +@lnurlpos_ext.get("/") +async def index(request: Request, user: User = Depends(check_user_exists)): + return lnurlpos_renderer().TemplateResponse( + "lnurlpos/index.html", {"request": request, "user": user.dict()} + ) + + +@lnurlpos_ext.get("/{paymentid}") +async def displaypin(request: Request, paymentid: str = Query(None)): + lnurlpospayment = await get_lnurlpospayment(paymentid) + if not lnurlpospayment: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="No lmurlpos payment" + ) + pos = await get_lnurlpos(lnurlpospayment.posid) + if not pos: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="lnurlpos not found." + ) + + status = await check_invoice_status(pos.wallet, lnurlpospayment.payhash) + + is_paid = not status.pending + if not is_paid: + return lnurlpos_renderer().TemplateResponse( + "lnurlpos/error.html", + {"request": request, "pin": "filler", "not_paid": True}, + ) + + await update_payment_status(checking_id=lnurlpospayment.payhash, pending=True) + return lnurlpos_renderer().TemplateResponse( + "lnurlpos/paid.html", {"request": request, "pin": lnurlpospayment.pin} + ) diff --git a/lnbits/extensions/lnurlpos/views_api.py b/lnbits/extensions/lnurlpos/views_api.py new file mode 100644 index 00000000..21c8dd12 --- /dev/null +++ b/lnbits/extensions/lnurlpos/views_api.py @@ -0,0 +1,94 @@ +import hashlib +from fastapi import FastAPI, Request +from fastapi.params import Depends +from http import HTTPStatus +from fastapi.templating import Jinja2Templates +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse +from fastapi.params import Depends +from fastapi.param_functions import Query +from lnbits.decorators import check_user_exists, WalletTypeInfo, get_key_type +from lnbits.core.crud import get_user +from lnbits.core.models import User, Payment +from . import lnurlpos_ext + +from lnbits.extensions.lnurlpos import lnurlpos_ext +from .crud import ( + create_lnurlpos, + update_lnurlpos, + get_lnurlpos, + get_lnurlposs, + delete_lnurlpos, +) +from lnbits.utils.exchange_rates import currencies +from .models import createLnurlpos + + +@lnurlpos_ext.get("/api/v1/currencies") +async def api_list_currencies_available(): + return list(currencies.keys()) + + +#######################lnurlpos########################## + + +@lnurlpos_ext.post("/api/v1/lnurlpos") +@lnurlpos_ext.put("/api/v1/lnurlpos/{lnurlpos_id}") +async def api_lnurlpos_create_or_update( + request: Request, + data: createLnurlpos, + wallet: WalletTypeInfo = Depends(get_key_type), + lnurlpos_id: str = Query(None), +): + if not lnurlpos_id: + lnurlpos = await create_lnurlpos(data) + print(lnurlpos.dict()) + return lnurlpos.dict() + else: + lnurlpos = await update_lnurlpos(data, lnurlpos_id=lnurlpos_id) + return lnurlpos.dict() + + +@lnurlpos_ext.get("/api/v1/lnurlpos") +async def api_lnurlposs_retrieve( + request: Request, wallet: WalletTypeInfo = Depends(get_key_type) +): + wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids + try: + return [{**lnurlpos.dict()} for lnurlpos in await get_lnurlposs(wallet_ids)] + except: + return "" + + +@lnurlpos_ext.get("/api/v1/lnurlpos/{lnurlpos_id}") +async def api_lnurlpos_retrieve( + request: Request, + wallet: WalletTypeInfo = Depends(get_key_type), + lnurlpos_id: str = Query(None), +): + lnurlpos = await get_lnurlpos(lnurlpos_id) + if not lnurlpos: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="lnurlpos does not exist" + ) + if not lnurlpos.lnurl_toggle: + return {**lnurlpos.dict()} + return {**lnurlpos.dict(), **{"lnurl": lnurlpos.lnurl(request=request)}} + + +@lnurlpos_ext.delete("/api/v1/lnurlpos/{lnurlpos_id}") +async def api_lnurlpos_delete( + request: Request, + wallet: WalletTypeInfo = Depends(get_key_type), + lnurlpos_id: str = Query(None), +): + lnurlpos = await get_lnurlpos(lnurlpos_id) + + if not lnurlpos: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Wallet link does not exist." + ) + + await delete_lnurlpos(lnurlpos_id) + + return "", HTTPStatus.NO_CONTENT