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 000000000..c761db448 --- /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 000000000..076ae4d9d --- /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 000000000..6b57bec46 --- /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 000000000..4e3ba2f15 --- /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 000000000..a0a3bd285 --- /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 000000000..03c41d674 --- /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 000000000..fc382d711 --- /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 000000000..ddbb27646 --- /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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≻kerdeE3SKxkJlY2g@^m_45zbnCcJ3a`qrv5d(l8_fJyjWH5*EpVv;Q zKS`Q_A>X$Q{yXFDc}IXBDtct_Iw-aGBkNhHvIJj}8Fo}3%_nXc6difoq|`h5 zEBA~q#z7GDBK9>xnxzMF#@j`i;~0^Sg%W)0kBf5tPadm2;Pdz+25j|-0#PFVqdhK$ z#_bd38Rmd!KYkq3kdZJ7&w!sYK|D1Ugc9F=h|}=DO{vMIl|f9Hqhg};#UR_V0l;su zAPL!ZTL^R)ffV~Wxt_U@MS(QldC^F>+|z{JU@2T>IwBFSzs(*nBo$NtH)&FvhGper ztSVJP($%e~KHQnv(dWf z_=WQkBkHo-i4TE8A73#he8o-7MWNaJfPX-s3O|d((FvQiLLLl-LcoM=1)@rSE6YqS zbIDn-dgFg$dsK5kXiL@Xm+cf2<5A6qmNKN|;zzlxP?En0gmLGj%|s=i6({2y&T|`m(|{s!V%Va-sRYu-sDAQrE#>t%265%J<4rFXW!Y?tZ2^RG{4O&B$UOFT5t! ztK9gL)Y6b9yf(G1-1I?%#M9lGGKUCc{)&Bh+W)#fUz7!`QYJS?&|6;`b!=e*SXn|W z!{Lo4$HJPH3v2h+4V_mkLa#DnyUv|+SXiuo;#y2*?Rc|txFmw{r@FPngzC7%jf zbiRoZMg>Q1{wN^V<%@qC8yNWMCmzFZKS{-c2PJR-9@)9iDOgshgLL*_`mEjvx5w1!xH zMjxsjcNi&>UjA(=*0b$rH40gmzxKMHUzadTluO%lEp~UHas)f@Y&mJVrr;CphMTPF zYlmMX|Diq(Aw3#H)mqA-Px|*3+_Vy8rBQ*5TbDW@z4JNaZq!`1E`4EihpXidF=iWG zm$@sw%e!(n?mAwVeKWc%_;B|kkInSK7*p-WhEHB{*5|(&GyidTH5ns_LqoJLaca>p z9o1W3_(cFLY2Q4Z*zQ=&`R+h2f^0T>tsW}XcA#9BKJ6C`D>sul)Lglrr%7W^cBwej zWx8D;#O12;6RmDvBNl8wivmF>Ac)NUo`!)vV2%sMQ>>6 zkvW-)epsFFhcyq>D%Y=mOUSI*^?dX5mu-g@2a(qoCp~+cn9Gdvl*e!0r)5KfM;=rD zqg8q=OWLvUj1r(zXm^w5KYtIWzesW?KX)rgJi}eu=g^!S=gI~zleMF55X`4!mf*E( zvhzIVkg9hhW%8HHt+C%+K8-!4Iz+zR*|;vWb}1hD?OEU%Crsq-nKbTrhuyX(bHihB z{m!w_;qAfefl9A$x>H3C0tfnde2X9640s&HZApuBLbKW(A&AQ+iGk+%r;pc_#cd~w zIqqYY?R!3aWTRf@6=XY&=^lV5*0@A|x4=i|%tgLw0rgOb`KR-GJ}av_V8$h&9Vvj@)vcU8KyWdDqr^XT0lRI|pD8qeZO@IT5xYJ&Fy;?l zJsrl&*j}HDZa|5V-r!3I(vz7t{(Oa3@ei*YRT_Uv##lMnGSsh*&?h~_h-4AhL&*?X z9>4q@1KmqQc0Io&5R~%s$lz>cbjM7|>Rt!Iwv2=Ez(#2Ocf_?+NdVf$b&|pvrNMc}2 z4>Z{`_^AS@GzMHyNt@n^=MIKRl>~g<^KYAUV~gg2<_oXrv_&3MSkD& z58Dgzy$#s{;U$zrIZom!fxIlLv0LT6)CRq1ImOSFCBI**UFiqoE(TSp1D`}9&qc&8 z%!8i^{f@UJ3KL^0jiqU?Mcwy8*h52k2Lm@ifalzhr`%8<*SOrlz!Y=9=uHSSX7pJ~ zOzL1%)E-`dYk-7#0QtUmr~;k^CALLu_{#}o9YWsbIoTy4(4K@t?p>a(+pa%OjU+EA zNolcL9XkNZj#0~wQ3ninGImuG#eW>|z!vsYZ%QgHPGWBYs=0Zi;U{YwyNiY?kEbYu zCA1Hn)uu%Wc(ME;M824IiI0KSPFUo3JI3f|Qg8CSH$@B_mr`B}rvzxoJk@{GYtp~k z(z6PCp-9Ebi%A`PO~Zo6%Bjk!?#RGrWj>E_jo0N&f)`}>+EW8^Cv#2orDnw$f)N?FFA}moOUVs?`^d{|%QVk5!)U?I~nsUsX zjz(rA7%tKnnld7VZnQ+wALe&k(A3=e^-inqd?c?z~0`2RL6`67Ks`i<20 zzdEnB8_E7CFUGh~cHeG)JS;KeP}{%V;jdYEM+fl>#6#Z+dnbbIHpQc;$)OaWU=!bD z6s=)T35TMwbZ(o2(IJQ8iEJSs?AMZxB~$sJn6=1Kd!?UcF~-WdG9>{N|Hx zc_YW>fIb-NC{0?sJ)FvBIX>!k@drtkW?{9y&tku%SRz9=Djjz4U*}a;X2t1vvB7L* zEh?koWUbQ&Fd?(*Vwaka73`G(yR+Z5%KPFYE8u>$KVN6G&f50ibh_N-fIzgn=hxxul-yer{Pd@-CNN*5G zC~IyIM50?_5KLjcXAnYS-{zRGho3}>#~GelzSUd)eV>o?zw{5Jm|1Nj=X z8N-7zR1hnO{&H3#UYRNa{zNPiOh|i9vz@4{3f@jqH!a;x*0x9s{K+0PznyBF4BkmI zFDflewSe#MWZ3pg6p9)QgLkuBcfx4x-LLm|b6kI*?d1j%TkPe9(n9z0Be@Rt3Sz|= z_aTX@7W;*%rpxq5nJ;r!akl?GxFk237g&sv$NS=_jn=d%OYcYU?v&*_r!!XMuPq;x zHOEVqm3LZ~GFJ8wA2OE1cHEC@iqH?slc1kRtZGNDc`IPOf@Q~vz4wePQ22HDVST@~ zWo69)(#j+e8yJQK`iLAZ0YWMi(nH^<>OkIOLKDV9&8!wuaCc|x zyzbt!NxW3$Ct|>|ntQoWiM=)#FF=uDiYiZiCIy977+f(BVS1V%)X>UiPCc{gG%fOFHF~B3 z3ArA%qPm=Gh=q<7ZO&uH!>-@Qfq8D|hBmK$h~1@pa+}@d_vI2Iy29~?u9-C}5YU5T zyL>&LeSN|nGX78K7D{5$2|kolcozR}#I~ z%doVSV$%*PF3N%4A6A}vKm7YN(*Afl^8xXAyO4u;y1$t~Ag%!$&!~IfQ6wc$eoN+} zydCUB=l$mgjLb!)0|~ePi|{8I%|-t<*oEsZufZhIi6IW^CP@Dm2rz#CL;inqT=BPv zam+wHWN`UluF*VP7j9w58I2Jp)tDb5Fg5!l`dO%$v>|>9sF#*1E=p+Pa;^SIbZ2sNF=kfi2$eCjBq{P_$|hBhXl1Bgi{ zN-C)kef~CU3&JS6KO|A1m{fgajHFBYa#~TF6kTh=tWi3m`S)C*;HZ#UiD6XbnLx4k zYoVmI1km*Tg@Q?I;g=k&a=rRZ6BMjME{xhSMRfg)C5UOkkwHXM+(r6suPM{;5Qi4T zA!{qJn44%W+_~OA^SZi-@6{wCIvuSu3oc|PwWeciijeR`o| z;VECkFACSGrnC!thGfd_Z$Ym~>3IhbwJ(k7zGIxb{ zLduSe*@Hh2Cv>Y+^5kP_*x*Y~BiwvE_MX|JHM(?Ml3v+^cMhs~1yvt|sx}PIRk~l5 z4PVd-_hKss#&Tvx@LGb#mVvNlScR=gnf9BcVU1;2rE6Q6?#1vz+dZtx_o+<(mDFMv z8T@|?OHb5eeqWQ5VquoXADEVg6|ZZf$C!T@LN!L5iR%)c%FTaDX-Dm9M5P+Lw<}-Tohpr@EQO<6w6|&BHkrJf zl}s`I-X>^(Esj+>H;nu~aldIvGG=uhkSZ<6hf@|%pPJ5SY0M52*7bj7aTVqnQO}=F z%*8QfRM3!mUVfXr#j?YGC^e!+pq_$iZR2tkvGEFU+tr0r9q=Y{9re(q3I|^8-$6cn z+5fhF-l<4=?q0`Ju{6h+(313`R5uXmGUqnaoa+;r5y>0J{BQkc0$A=wBq_5khhKL^ z^hPAUX& zX^?J}l5Q0N1tbLN6lv*_66uoe25FG)j>RIRq*DRu?(PPGbzXIxab_H6&ffbv*V+H^ zBVV}Sy`SfP?)b%Ep*&M{iFedq0t(Z7xN2ChZ6@FYaNeEfaEv_9+*j3N_elTOW(ggf zj3~JxaP6$uR|=d;(6jQ0>#Q#{7@STcxhkf5)*tf@fR*&DN;#f=NT$a173NVn4X*}g zv0LXn?pb^ChIsHbJgx!9c&Kt(#ZYBtIba;OzQ1=iT!(Ix@AFbc`#I-8yA-690B=d3 zqj?m`&4xc+64R)uWbFMr8*n*~shN)B_za0Whcfq;ng759H+l|xM}e75(Y48~2Nl(e zPq*x|-b|gCanu}}Zre=*#Ns6mzi7!F_iGc9OG4_6EfwY|=;t%uM&_A*t6j^o__I}Y zxQrLqw3fp=Vz1U)H;EHj)E*Iw_e0@Wtr68MxbYsxI%z#`HRh3sjfTY1^RmBNzfYBT zj8ZFFZ~Qurpm7JY2YbTnEiK z;k{%Yy&SXHsrBSuu)cTNR&iY_A>W6|Xco0+xUuOj$HxIY4ZHsF+h8=a{M9GHhea$G z8w%3$#QI6zZW|OJ$D$D;k6`Lr#nr7Eg7&Eo&ZnH^6jzP-nl_Fxz3(|WQY|cq#~@lP z-{*BXyAA1o@Uk$x_IY>gbjTs}=vuhr#JI~?L%jb+5G64B+R5(ZPY8=(@g(e%hO_qepB z1@@~zBlM{wDn@w5idco{`WQh57G94N!RzW>y=Oxx0Q+&65|cJlTHIUzE{<$nu`aBT z4om}jxw$tu*`u!4!_dwf=!-5J+=0dn402&>cYn6x!asrm=0mNW@p(q*Tb}F#nehQS z7t0u5D-qyN^|puly7^+el=k{qvO1QU+n(CGuB1z^dP-eu(q+MsIkwWdz0pI^Ap~2k`I)V^e$cPl~qRcKK=ozh#QF{PMyFI&-Pd+G+Cp*+^#mzy< z9oQgW?)YASf}rNk_9H$xM1eusz6?sv-u);>$td7x6eCvDQF%=`Z@YUwikDt?$JUy+ zlx?rIslSFcYfLvgG~N(Gpo=rZm<;$Qq7x5zjf4UUi6u5IKMa zQUVxaw?^^nuzA!Y|~T0mhT?WF=Bh^K4 zezxR^!gSaBKt!h?J9u_gj~c=2qj3DTo$e!V-ASSlMRxCv=G*vpz0mF^N+QKOV}HAD z`G-yr|C@p7U)C*^YPS(YZ^#$EvgcpsOu>jSdsV%=K2m0U?5n1E>^_a`3?=0at$Dk% zh>?0+_z9Q})9qo)3*~;fyE@y|_PpK&FtaHy7gcKCjGlJcqq0kOKfBRR>W=zoWX5w> z6e-yYO+iG0+AB9+v0^rFF%5#(NvBWvC|PuI1)K*4FZhTcPyp`QRa=^@2)uxQ0TQ zjn2Fx5E3q9ans)Y(2{m+&yAwGrL(?5tK+kYC);7`gmT63BomIaKrro zEKqnTP`~q;^ocRV!;PcR$rX=E!HRA)KOoRe`ZJgn@i`G};LlyEZr8wow6=sq zX`@|n55+)y46r#>>(uG#_j}{gbwA#sEASl`%bHw5EiT5Ut!Rlme4vI!bZbBNx+Qt0 zsCNcB&VYY-;{&36TrOKgP!F5q@C(omL_4P&*VL`JeYc##jSu$Mn6j4gvOy{P3v$h~i@vAM2HzNkH*?y~@h$57L|@i5@P=Y3J(8?1ez&675x zB=t}GnpHUy>_9oUV+H3AZN#bu!m}9PF5u`)?l0deI|a{YXZ?QNGHmv7vvT?Ky2XsB zqydrTe5d)#x~1rRuN&wfc69XO8`1{y?$rWb6-gI|V>;U}k7q1dE{_&mcWK0Tz456+wawmLR(U;r5OjdLS7bfg$Sj z=mM!m*)JW$jt3c93ex8e0`m?bH@S#hwm|s#yw{bt$!I!+u0$x!-giy31U6)w2+lCQ zc_L;ppgt=PZ(uj$bD#ULjJY*~Si^i^iiNl=2wgSMn(+^NH9*OR&BTyK{QQ#^+^TjV zV&Zgn!f|aWB@qTngcepDqoRax`&t1&p-AaX$ir^dR1C2EYCUG5q$nIFd9yej;_D_L18}y zBs7jp!ye1ggdb%qby3Kqk3&sBE*htVin$_FRP(-PH15rZw}M77`7XQfcXLzYq&*AM zliQCQ>CR-Y=;$$X*pGjrmBQI0(qa+3@BPW=;X{X%Rkh{oZTT$japd<7@9rcX)EH=~ z*+U;n?3MZGZ|I=#i}rfp0)rk&S^)x;P!D$b6bwCDez-HKdfdHK-)`DR9JzhL76)l0 zSLsBA>HEXO57Mb6>4e{Q^hZ@6WUvP1C?wN;hy(O@ID2xW78?ab@td>YVAcfja^GFO zZ9$%ELVL|iNdQe{_tljPVfX-~LFYP|0RC#|&M46cW&|+bwHWa~SWa*T8lLJ+xZ>y{LIUG3|)c46<6wg4V z=iV%V$s99fdanBvcxCXRF12^=^$pV zvglsFFYgy#R)ZzU;xOzub)+y*`_hZWei2pn2&FZTwYf;KEO+`k%8PpW!{^TMXfrU- z;reqbR`)ATGne<3-eQ=s0;)M$4aok%b93d~TQ@o8sv)b(3KM*H_c zT_JX@8gzP5GvdDZ~& z7s)!Fx35gbgyI8z{HaY53Ai|kipl7fqg~TDsnS4sq48{pNFbp%XVr!A}e7L!?F=z%#x!~@AFDxUmTpT%4`=hy5~$T!+eTNc(e?${nX zD!ZEoe#vVhG!>wde?Dmxk{ABR7zx!n$pURfwzqD2h1h$WZ&yGd8dZq(LOv zMoeGtiLA&oD~LSKpr$*uVF7lo^w`e<2H^RfmPfX?S3Tn2PfXa>D*(j)q zC^#UQs#B`|yb32}-2_+nU0$77Z zzi+F57sB_IsTd|g zowE=w!7i$1BlRdYfu~+#w9}^jLZB1BK>k?*kFH1wd{oP*fIqG_WKS=~FkDOeg-U9b z5SKc-K}f8w$=*_Q{m6&T#C90^#UG87KQR?;)>gBUz3JDo(_v{=lwn|gaCr4mCzD632mEdvBKIT7{SJNBs~JaHXS zZis;m+mjx>8f#AeWD+N&MLdE0#U$?jWa^VV1b3=8{x>T1x<~%|CUM79RDelb6!h{n zevHcD+E9*klg(4&BbKoggEstvYqkcCFjhX0VU!b-zVvo5@ybUIB062yBz;yR7I@u0cLeiCR4o0YluNW#K|Cx$>>MD``^%e7l2FC{i92xgxT?*dT9>V z8#!*!c-*iH_fQEJZWjr5)kIh&giWW}dQKLxSPy$5RylQYy0W@{)1}DC`QT&kYs6%8 zXe9y2Ws)j;#{CcU-Y0j#aG^MP!0Pc(z#M*9m~P zU{5KzETmtJ=j`Lj{`&O$`g$l7gg`sv3WskvP0T9RZ9xh>Tp#@VY&NQwEQ37@`lc)fmecc6cs0k{DJEM3O&dYVgEiQKA8JxW&Y34{OZChnMZWL+Z@~E#Q7V*!~GU85g1sI;dh@Ha(*&I z^84RAod3VnWtKx|#wq#mn^ixyZ2er9In89w{jo0ll7-&Y0tyl{O#w)x(wEt_<-ewA z5Qy$33gLCS+AXjR`uZu@(%-W zktxGK*j^-Yw5i^=a}Cn~ce~Z}5QVjsjBu4Wy37cR+-F(wp9J5NqSolLOm(YT5}lVu zv+@c`p(yM5r9)@A1tn9RG;}7S;n^t-pG}O>jRF5+l01{rZsv89vfc*_rr-gD+^A4% zit0>=iG^xrg{J#>My0Kp#rVc*oJ!+-3SC5Yz>kRFWBCG zGBHliQ8(U?W`0xCfuAN)((&%WtBd_n{jACTME*1(+B7Va+X!JA!YN*zbjv}%D^MJNBVY zb61jfyM?2csZtEFePJ2W$=oDG`1P438d3e&(uqsAy|=o^(P0qlnX4pFR7X7Vp=v9Q z#mHgas=WfZ5Iz)vmolC*Xeqd{9HBk4?_8PC=0dfPN}0}ky&uBG6X3M-A+i?-8sZKf zc@Oi%_T`JP1b|X})4i$&B~XRJ`EZjsg)sL@q$nx7#se3#S5|jIxdbVji)~lw z5046D3%Chg>3nh)9?iS1i`-|>E<^k(B9)Qq_7zC$Bb)8W`VA`9v<`L%<~|?}Pt86U z-Br4AJ3KC5gY(oDFfqnSh@VJjg(u3XrrLkqCZEYYYcFPyNQW#LI2uX`f;K-pdmIPy z(r0YaZcM$w1aP}E1uCj~YppGMUz2Yke!P3*kfEONn%HosJcPMCLPJuI z!PiQFXzb7nyE50Iy~sj+y8Ni3>TP+UP1(5Gb^GHtGgpfM z@XOR*+;I)*>XtGr>@OA9kBW+aFe3Yx)Xvp^`S2)0#*!xR=~R#4j^DdN(R8I5U{FM6 z`0CF&CS2ie#ArURM}s5xC>YtB@q!VlB0Z!Dd$Nj94Dqr!3gXOZ~gh58J8;fY%+d$l9Ob<%iZB*SKOtx8B=s@ z!K9Z2QX;cv=>ABWpc|c8Ut6Ki!Fe4zw{ zD__$9Al>()#*6a*`^F2&m^{B_=ASfP31m6{pz&I%|E=-*n~(FeEB&jk^yiWKwwM2T zq<&pM{k~=YIp}x;*&MI5T%YQBQ?Wh$4+k9$e|p4zo#*{G2OSqUsR1O_Nb^2-T2JPJ z$c9Ezf*HS^=cNm$U;q4w{n`xw#svApBbJ%-`fQ3VF4EXX2>hC7SYTva{WN})h8C*Q#lb=L0BYQ za9A|5Ag4cm8f9_^zNiVgD6FXGs(!q=_o^mt0a_J25sV?fKOCQVEMF4R`RZ~s`Xkm( z!gvu(_&8*f(WNLDwod&tjPMfVI8{S({`9A?^oGfa=(M5&kMZS=yk&_ajQI1^SJ#Z8 zHPASXJWALZEiO5@;?=_Hqt5lBIt0{>;zoS&jgnUCfQ`~l_O6Yxcf<^SOePmPAxxVi zau|>?i#Mi_Vf~KTf-@k>PyB9Yi-bvVL1Qg$Qo3Irnc3aR5xJz2`Edl#SGTZBWZ4}WGdmSWDZPrG{ zijBQ4);7ldZnj~Q{T|Nc{5w4i+Z+4u`4E{7dLJH39Q29M6dd%kT-#*rl9on0?2=XG zF7JAx;bh&VY%6&TtUq+qZq#O->2$$#U-EP@hUJDmAQ_Ta_Cfa!iNjplO*4mi zw!0_e1FU!Pw*a^P^JlFD$rAKr)0klE?Vh7W+q|lmsAGe|1} z)hI%s86iwG!~d&X{fFNB>lzY8NbEan$n*egxZ;-2R~P8Lt8Rc)%oN}AYuNlKSEFD2 zek=3OBKmLG%5407MD>7(%AgsBx&HIl*Kn5i%BVJmHzUeGCkrb?TZe%~-4G*K7aTsz zn+`5T4kp|(#2`xVwVUPTz^+>0+oY}14!Dpz2Zecn&kxcW z_zHy~qX5y`E1}D;B!8ch{24zsHkqRL?Ol%mm4T{2#GhefFi6)${s#Q@@6P`n#Q0IUh1* zlEi|z<%N2T~V5(APZdz-~#ZkyrZrig|US3f+ z4sqB;LGNZan}s;yU__zDlxWoj;E@KeAK#h?fpxj~Xb@7i{4SX{0A11nyxRXeALdDw z%!ZyncOnhQ6kh>^<3k1Zw$^JkMax6M@gMcKp$!39@1>?3>^GJ^q-oS&I61C&W_<2M zUQWPof9^!KPA=gAxJ>tA)|BIh^>U&+;_a9jTU3FOWKRrlf)p_W;K)|ctnp54I#mr7C5NT*7IS6+yJ0+NHYGvM*t`r!jwGxQ{Yq`tJ313n6 z(FI?UFzZlLabm%Y2nDH6jFP2_jOeEd$V)NWQGQgh22n)cAYgvfaQy3MUCF>(h52`H zmDj%^vkma!KJUspi2lmIl0>J1=gEp12wLt}yBVr8cN1$^DLthafknN3Bm2u72DkHT4*TjQ-%(5fKwlrLvnlTk^GDNvLV{sZ1JGAX3SGJ#(pX7V zS0fZL+|Rli^pbzfVI-z4IIabh*x@2D@suc|C0ae+LaIlM%H`VA)skEln<`bdt4rnA z6rXFzWA5tE_ zdeEbCcWtmxwJGtUnq8j`LQ!$n1+jfP?dZ0DDamcOJKOr|rUrA(>5Jvg_=nUZTcy^Z z*b?LS&aXcnxlO)xff5iVo}X+_6~!xtsB?eZ+Z;BIPI7iW|G3j6-0;-gb_B^9rZU>q z1*<*EO_%&c!xasEMAHp}giy;Jhk0ej3sajj3ircRwpbHL3pIr)SrjK=D4z(}wJ@eC zU30l2+B#v$_DB(6#VrA>xZYhmose2mhbr38y1qUG1#v!Vn(7cbc{>Et4m zm%^9|S5i}abF&Ro0!YSIOsiTduc>IG2)5$u5T4Fw-iPhT33=_j_AL7?GBTfaGR)4a zSUHmyi+L(@Py@YunZ~%q0@WpYhN$YHIOS;AQ-fG76!biDuQrvK*-~t3W5%*}fjpD4 z{s-cl<$Y#`Yd#@1T`LvA?(x}`Q{Jk?#kYe|)~eSa3~O)FOBv{OsYRon772A4P1Ql> zy4DLXU?tY`D^b*nts7v;7}Wyty3E1mSR%=BR%@e5+V-Ekrw z98)^OPhJDI@>eb^reE$C*v@#8KB$~^_nAcDzm$P{l>;k_?z$L5IxxF*<%OBdQlu@j z!%C9zgR{jf4`=JO{OH2RxK;YQSiHJiV!lc%y?ivqMh}`RRHJ){{%TbWO(ygy;?*X zKbOM5O@3Y}+;qN&kK}d3FmK$$4Ci29)s>YX&PbPfY%?&=EA4$K&Q(y{k3#pvE8Db6UD46=)+(;*<^I%`*X5jst{lIq`Xa$MS2ptqy$#h$iwd?@=gu z3x4Q>+!p+hN7XIqk#Y_>nNiv(Ihl!{Ks>Qxopd=#aVl$>c@ATAxv9C(IQqP@YX7yo zf<*efjP&!gY*pw;X$OAOAA0vksGHz^1>mL)=?5^C>5kxJC_}+ztLd+dZf|8Sg$Ha^ zEtb9!s$Oo)E38@19WSiit`^^}-CbK}sr$Hux>J9F9=J_i$4tKg8DsC>X+jr9+ik{H zoY*WfIo+4$3GVkdE~s{JT5o?7&iDz$lV4guS5{-u&fJDpmWkAcXVQYZ9Jv4P{&BZu zlkUm>ra-zbUg4V)>Y#&uF^-;t4^qNc4+lPhcm_f#0&l5u+!RNk(>>qVAJLAMJQ|Hx zJCADJ43u3Z6!N$MVo)u<-Pu*ObhUJf^cZSd+U*48v7BMTs5{PAsb5@gY=L0A&RZh( zLPjIfOzjsVY`NH!h4Zde#H4}u)@tBhnVG-Mj!$%`&D+mAThDLWa@YXBfADP1|4y9{ zD80Q0VI*CRnR}^;00YdU8gKocug*e^bJmV@>v+E(6`f~h@G$FEY2rb zh_$CmuQix*2RHIQ@lWenT+S8A@|90v9sxv zZ}rNJH>ty2<#@X;+v_m;m}qNkE>FQk041isF^!V{4qc244)s7IqA;xhmXW_Fnl3q# z>`FaoY{z90XDrp7ph?y~*9Yxf>k8BdNzkqQop0$w?ac^7F6#TYG-Vhkv9CbhLPnZA zu12_1s+?qN#hM3@4Y;Ek(6%zU%Azh2lV`}cKe|4(6SVj+agK2D?Tl9t?z3??x<@bC zWM{(AoCb8rj_umGvE@QvZrminsA>~J?+@|CO}~jp)bU6GJ3O#?;U*1Thqy(5c7z9?QgDc0W-q@dxn#N@H#DKo-nQCfZiACBah}Q975mOy z{f@Yc=#ow%z&{<^Pdw?)dT`vq@ZR}sKM8a-n-5W}*PZwv8A&o*;8$a$Y(W~aK7SSP z{XJZ9zHj7`|6%_8-!GS3`9GFFqdw(NH%$>lpg#G(&Y$xJP_&3NAujxZn?k&6cCw+c zSVUoAj|4myf+WHh-}6bkSEkE8UXrI0sb`}b;)R}ThcIaXSrh2CM3-XqGyQ=43F%mh zH)};+1}YWN zYcAn^t69zt#Pr$ezAE%NnVJ4;xoHrG+`QaIXy;lUxK(96AF||coK>)pv&VospB2aMsb8WO9D5AZbucyr6vCD1fPA+Cf|llCH+ZT)FHoURbqmqgwc8 zBX!)Ya=ZUpQT5)^xENPmbvaFOCpCk41005l`P;~?oGc#qHTw0Y3V{vtW?X5L-9l^~ zwUXAVqp@wlc*7Q&M#N9EPwFg2+Ol`I)t++|G}o-u9%icU*RX%czf;3KK5qGrcb*Yk z$AKeh2|3qx7^-(g&=hBY91xXt(<&D14JsHESgFK4;xgmXY&8k^a9bl`cl27Csm+*4 z(I$Iajq?Ue!^``U5T?xmfxAOa2GGglX$B64;j-1Gt9QC@1GCjRzB>=t>Ou)_RLun` zUREx|JPrm=h<_BRm`=U|R4T3X8_z+Wc~?1_7wQVm)=i5@xHfKBR+TH?3@PMVeWTH{ zH?tWkR|Z*fxL;7^F17w_9ZW(B?t3py@^a~eF?#L%BsCL3Q#{;#|4QfZ(hhK=&-C)K z3(EV><9IuIr_=3yW|!?J)6V!YiQ#>hmuDaEwbH7Rwcw8l``AxLFGpLxB9ld_Fv(~EMY?~ zg2=tP#I-<&(R?I{xgKzUciQ1I_yVV}=c>Zz3fS+?7Vtx&rEL0aGJsoNL6YDHU0fxAaYQ=ZWS;+U||@1=&Ub3Sil74~D`* zTn=nd|Q>!ZyVDFgNVe|FG z0&^#;lWmQ+8V``quA_iy`_dl0qIjAMru&d3l_v0-zMOtAPtuegeEfcj#Sv2Dj&}{h z2=r|AagqTx`D2jhJ+0Q`QJZ6B7VC2vj<$tQD;+MjF5)X#6g?jtPCfmsg&2JEVIpJI z?ZvJrs%f@+!|Ia`iNo%f*NHa``!c!`*$sJ1E|&{Hi6zNjwPID{BOx8Gg7vE7+#dPVxYj{EFz zZM@8dh4;b5hK~oTeqGIH;rR9hOZJyfR7{HxqsX44oej;ne-@4h(<#k*5QxR3C?~=g zgqnlAX=i*XqC`>^;Zg_DLE835k-X}owo`#yeVj=_w}-OlgSeL3=R+PK#3tI+ld~)G z*Nrq$1W>cvr@oRo6V}w$Dj6A6d}*+#*g?iMS_HNE{*Go=xQSF-KTX5e4*5&8DOzBN*Sb4Y-6i8YeqNdmUdMX z@@uh;QRxa*h8a@)iQ`tv0#HU7%X;s}PxBW5WLZc)xZgB?%xqlh+FZEf24pIAe6yn5 zH(;x>yw!N!0uFn83v%oZ&sch%_@1^Zot)}5&h_(qcMYu z0?7EYw%QL9R>P*i7O2TeT0pEHg*%kjWtquN>f1)^ClluXVoCa6W5Q^C0*H`}?au|0 zHJ+pdk<9%95K*r`7a8IeMBqyg5FyUd{l3tTR7aYM2+4P*SNNw2vGu*rXkxAB?3ZKg z_o}jDUc#O&hq+a9H8>yM0Cj_}qPw3Z+X3RsV(8osjsM*BynNo9d9cmX+&X zYLuOqT?nN&&MT`^G0q1s8RZrFItYqA$v|M(D1N0cwo%ef&9Icy$-d5*)4I1toBv@- zr2rhH|0WwUU}0QPF~Zzs5+=-ix>2=KN@QBS0U2Ma+-hZDiS2fOvtC=7`RsY!xq3iR z{lymQP6MpLo2?q8Hb?VV$Vl1}3p$PwMl-TvV2q2eBp+ zACukA%tk$NE|#{0l7QP|A5gm4r2{RykJ=}8-t!EjSu~8}PZsn|Q`|7CG9TQxJuPL&>6|2 z5z9;up5A8IQKYm4F@#Io&`0PGPWYa(*DcNm3!|SdD32X3F2Yki!(56dZMI)Z^qD$c zNq!~OyPOrEULBh6aac8jIQijh1N>R&=L6MEUqg=*hHdi8ORa5Elx^}|65?s{xhjs3 zsjYXyEMKKUKlv&EQlS^eGd|BpqIG{=m;6Lf0_@_i_wqY%Hw~D4fYtYf*C+ZmKh9I{`N2i}`&X~!#~bKRN+kca8vcuD!!Jyw zuV|?wB@(&^f7E9B2IQ?L49Krj`KmgPWcX;<5K{Z-m?>p?8|cR@MVe$n0H9;*AC*W9 z!k@eUM0$Tw-THk!^PMU%4iF?m061PCBxVrI$jygqiVTkF8>2>pYXo$vAW$gmOsEzE zz%;}L8-&R-xDir4x_+d$#UjGJ~!P1YDvCBHr zr7|49E1nkLKZgVTVpRF7=GQL@I%)jr8-JysbN^5}LcUO6+Uqg;07?|>(<&-2C|Cf6 z=*a0_3X|&CL+@B@h>kd@KD1Ha6B?==@Y+(~_BETV{5bUgB_!DQLmx9k{R;t{?lWWfTjde43s^urssAv9zd*!L8l-9ytLI^g zDJaIE6RwR)hZX(|predfE=6ORiFUFah1%+BZkf*-#i%#x$#-pc za#w|Ye^Uwj5tcn2NC;?ulMsYF{tXFX@kggZnjIGEhj~teg#h~_owz^Z1HX4FBEsDN zQc|mz_sYfGLWDd7K7(%)kQv~Gj^McRKeqx|Fs7H(ZwzVE;j7nTx>x~#9Jy8|LG!Pe??O1 z!`kBVOs*&QzWjRu$RAy75n{UAf320*G0$7yYd6Cy0|_UKkRmbc0<<4R79*6#(iVB2 z5ECv&z}V$1d~#X)6Subi4Ftejb@UX0op0BM5}yhqKi4ePtWS5Nz0PKBYd?Qc~xP$GeW zK%f6O&=e2J{ta(v`ga0d#C_$$OJ<7!T=dUGl>Z{ov~$$Ve;i$Y>6Ud3>(QQ3Y9GOc zE`&*Z(#-mk-t1SAB}Jy#-)h?*RYm_OqW_Ak`~JHTg}v~?++t`>{%=K;lY;IqBMSW# zQ6YTMD85@t*XXm}+~lbF(7MIV7A)(oT>3an1kqYNJ03wShb+CH`6x{{$!e7@SZEY6BsHt^#9eP7n+doaS>fR~J~(tAd6_ zPxwLgBIRRn$v60KUqu|FzE{Cae_#Rq^GN+XQvbtiw4X=n=aKra9jR)d{(=Xg0QDCk01-W* zDJJ9hK!h1#;0)Dj^hP8VgTwsF4*8o(_Gf+dv%dQ4`sxJq3WNqi1&*0KQ08ABF-ith z5kMn1KXgX%gOH#XV_>g=CSMRlWVC;6xWN}nYsN^+n#u!`qZA>x#HMKUBklly^zBV7 zu+bH$QJauK~%;6~qS`12qFj6#A=i6{0AW znxTx=LvN&;*j7rRbArAEq+%0<4Rd@Uu>3a7C%ZG$!Jmz*K1nY9*|>_&+Kh;EK(o(X z_!ah3qk|b)cj;IPYx*ZS4cUr0?34E+^9Dg0DMp)O3MRduocdn+;pLC#tM$~q{#b4{ z_{O|zF5}IxnR>kg^ymg-CF^v)X_;79xM#_HYbdFBu!Mc-VrLAC@w;yOUnQ3g7Mc9s ziorB|2N!5-Muwe1X?`Qw`o&Ni45fxy%{`0d&XhYYQalcat3zcv)6BIWt+&VCT0)^x z51j0W_!3DXlbs3xGbiKHD0f!Tk0*dbnQ*KFfD1h9;~@CzP=*xYit-PgQ0`Kdv`}Ni zk-^jH_EsPhu_A;vP$O50+zjE^QSpf}kdKCY#v&s{@G+o^-kT4=4sBd~g<)1g3^E;}g^7a^(bN!@ z1HUB>bPDg+ab&l!zZs>-Lmvt!7m5WUlBIT|@r>_nQ;9DC-z1~+zpN5su%$QX3c6BW z3Qy#Z06kmd(?{2PCaen@77IEy&DDI343gCWF{5x^2cb-^gr#m$^#=3l7LKAM%Yn7^ z7)(h}qh8dpXHUc)AV<4q6dQ?(8CMt)W7EYkw=QcxgppZ%as*>eMbut3zUcwJfe2^P zMx{7uJc4z7r|$f|VnV~QG7aX7c4Q~><_VI(ofbla{N2{;wi~-`WIl|0?Ud0bdmXe{ z`Fowe2_W7v=HG8It+tnW2fwSf^p5Y^!kq&CW3_|#0;G~w{SwzE+4?_=D2T7q%En`Z z@0ez*8uJtktGP@b0wM}Gjz%@JB-zJ|^GqS*CiRm?6Bffajwj8R0l5-KVW?XuE)}1xLrtoAgkYnp)K2(_b-og>9@4!NgLE$MNfVfq)P?xRyz6ay%PA@O4veemH ze#O?=dNCk?_zSn^lZiJe)Z+g3)v??6chJZBgPCB?wK4WOZ?c5$w4;(X><#epa_@2l z1yjcjT*Kf2D*2a%6m-0O z#l($zV37p~rTPqRCEvi^Emy7=$#7RNE7$LZx_hK0BM>0HO*EG=zlN-d=%L+!71ygv z#hrqp*xrZ_%i_iCDUF~*`w+8#hsTRy^e{1^A*|0A_waGp4AfbxDKxB+1S%tPn9D&y zd1S6@9!Em7aZBhn_@O6h+;o}#Ny)4-z3)SEc*KXgsAR!~zK_> zI$U|fJlg|lUumL?!?i`|VR~AeV`4r>B4xtfS0Gy*7SI}GDxYnsOClw#fdp`}xb1Bt*@vGz@#q9jaflH^X21`Y6> z<1AP9_Q>#izxxN?2A&#%?ws7)6b)Y%_x`Bny>^)A{l_g?7ue9Q`+zc(LuJy(IDq zHELFb=t_|n-o9{@PhaK;BUFxTed30jkR&i+TTO{ABg%T!7>yjc?RIc;3bcV#LRD5^ zfe4Z{E;tFsx66DBMM+Q3F7w?Ta}_LjDqA`wp(%U3BH!fq!Q0i79^v(v@)c^ugyJ50 zTPgW?Oe$z~X$oPHKO4Oa{Fx;<}GK1B*J+ z0QaES9j6NO z@RKT=ZaUxcoj#sTB70m#&|D&$*P)}mR+)2e>jkUJ`5Us2O6`wGZim}CEl@KM#chQZ zIcJ@}IR|)x2xSpv5=;AWr`?89WpSw5_eUl@pbwRVo>`Ay2i-3k z&3hgtk|?ad9JCQj<`Naka(-(?phEfGH5?`%C0SI3-p0Osq^BEa6Uj#zv;8p&O^nX` ztGwhf*hPw9_+qM_j|+@u2BwRrR4B()+Q;W^ItWrJYa5Q^(J@8XW>iNjVANZ0-aX9VWm0Oc#s}iQF5+BJ1^{uj^g$*4Cw5w3R?1M}U~U!#TG>dWzM^oFI&&3TKv zvf&U1;a)mFI#}kR*mKlc?TR@U{|gh|BHKQj_s`Rh_ivyLzm)aGUr^>JP=z9-?s?vE zz^hgA;x!_-C=?!>zmmn7*79yA1VeDX+`N*7kjK1`mkU&vECk|9Jn|fVJp-pn zRlQdmeLncGcUIA4LMp`h(k}mEqNi&*wa~fM+el!XTsTri+G!d`i-&M)Ct$ddbO`^X zlZpgXhH*Y?LHhEela_^FlKR0R-&FTAtX*~2mzxL}H#VMOZGp43@M;KcXWpD+I1qZ1DJC4fY3NSi6HCO6*V^q4rM#t{o5KMH9_Pb61vd&#LCO(1suj1!LpSfoZm`jumXX{D*}P?% zW|qk)maM2)wt5e!zk>^*jyZ zq`v*=N%j+^YC^F>vJ?Vw$PltyN5+^?-{tX-u_6<&f`hYvNWdfHy>46Bg0py!uObx zhZoTr1>l`yKR_Q0*qFb&bjSKZg-LI}->fC~2Lb<&zq1Q>1@^kEFt9M+mY#oBUBLO@ ztlvfH2C8S!;%tT!ZKY-;sTVEMO_TE|Fi-wlKx8pLi zQS8@spBMOKPN$|Bi4GfAKN~D^CgCCy6E-+s-21HGh17tLiLdm;^CMOEtS@C05~%~0 z$`djd>Q6qRV=wHIeB^~_DFQi{L?}FQ#E`VS?jz7qdfe{keB!TK+?MdLKV=0kK>W7- zRG_SaXlR%MwP&IykACbB^@;r{W{7swsXQzub2OY!i|DRSjA17yc`PiITVgy+^6G?V zM^BD=!pk_K$FS+Gq!1gL|Ln|;NnBW)l%Xy*Aj4b7tWr+V&hiB zr>Its^=H6^_4zX`S;rX{O&}y2=A?(-#w^JScUz&(O7&-qDew%?f1@N8fL(ebnzB*> zK?vQbvs_wT<%1-T`z8r_j_M6r62$e1^AyNeD3yxe`e{`!6gpuN5Sj(cC?DTo)bXOgcUS>CEo@QUVv zHBVCdjb0IqT5!J%RPAtJ=gOmy&e;*f5|>J z${%DsVfXl|?U17-l&3<}+SU|2`LyVD#iaVp%VTi|7G~3$V1O^b5TR%WXm4|<W?mBYY?%J1!UVBueaI zwdf8z9%N`KNW#86jte24v=X07PmD`g3#D02R0EMF;xSgoW3DNlOD%yClUY(6Zgotk zn-V`cewZ%?SQ9QNMv$5r%TM}xDsBj83$*~2yApRILOq4oHHX~*O|<#l4Vb39bD2Ox zi8NyVlYkPd7eVHMY1gL-mQm%*tonhi??Ki7KN?cFN*jK8<%oYfglHOT+U#71{x6 zwoKx@N5bdmUIW4fC&S*OrH+j5bwzj_y zXUyrZyUghFccP{|&Dm(qbCl@I6c+ORkSq8Qw4#vZrE6I^ zy<)X-yX2y0Cw6G;p9MG!xJQkh%X`d5;Jw zVgr>P=?)eNrHlEC%|3aLLqw&tj<(`WSlLTR&SqZ2FdcnqUuVk0EI=sPcW`!;A^&7$ zk2trn&?qj2geOOorgJPKm(4YSeL;lx|B&~VaZ$Hh+dnzPAPh*ONH>B=NyyNh5(7vK z&CuQ5B_Rz`Lw5|_jevp(NEnoq5+W@y&*-k}+V^!|dq4l@|K{HJ`*|^+&+jzn;s)v-bm^x?)L$uPN<)MG%=N%Ff8Dky!OxBo^cincmm@ zdyU`X{hrbfMMT#}0C%IgT4yqmH{5nVaO^tPJe!-+5Ko1;#V_(^d#Y$X$|Y|tr&M)) z&Ieq+jM#TenIfORnSC@tL;XD|=xRqp-FCK2?WEH1dKWU)G2i<7q_)lqQFKdmVf6lK zGhZmejmRD>h)|1GCcHGu5iiwGhp z)~x+HBGl278055Zck2A5Vb4*fy3Zy>-Ng|2bbgY1r&<#E^i5FUb_r4U?vp9snNHf1 zx_jbZt$F-DFOl0eRAOKiRd z8hnM4zuS_PXj4;uP)$z&L+eGdc|QnIg`-+_5n88CBiefHce4;GYA(3kBTJg&fNV30Z|4WrW;j z1NCSSb%R4N6heA50`DmRk2OLughBTfLJ09#%lQV8d>km@`+RAw`b zrW9I+PTvMJSfA$zX31HerC*F1^8$- zA#%D(3o_>YOfC{g?}!F%pn8jCAHV6|O61mtsyAQwTFt%KO+cW9R?#K!Y{y@CM?4e`yAF;**u>QcC&1m4jzMt)HgTQa zNrHl5RVz`!#OjDlKm{*OXI~JyaDe{21)gBUt8gn!Ipx)Ji-K7~D{{4EEbX_csx_om z*${IJeQPlx%09KB1oXAi~MA#%sdfZ4{yLy7F` z@$LFDs`>!StcDc=ASSk`$VcgS3j^t){^oc==qs78&de=+K+#l`a4VTCdEz%h-j5pm zzmclv*m^&Ep!p>=6Gu0Dlj61ux%~^Mft;N6E{lDgt=m`a*r@a@pk*+SIGZC)jl3iW z@hAtlph0JC7Z;+5Op>lW3EpN>j(1fC^II^-sAMnXDhY$u=T!t=s)@TlYY5j+rwk%N zZW6iyA|^I0=6#@A-fGCB@V!21ym=LIev4(9r?=Z|fs?YdAz*r8vwBWmQswvyi-_KQBXYrAJ%9y%D>H>1I0GDoU;1s`k z{Dyoix;XZ?+&z9Jce=@AFN6_cMy!&zZP|nRKgFSgV=n^Zt|-+T9|= zlfD!Vv%up%oTfhDd+N*~4&u&q|Jn``!e;;WOMgBaV9{Z~Y8k)`95~btpS`TYowJ+v z%3iC;N)f7FqpHm=3^HN|!svh;3c0VOi%+t1zPwePYEtQh<>ro;28p;!4@qwi*%BCP z_DtgsK|;gb&0n&IWFD)NZL5z8$CE)Cy>zkypb0S3u%>z-v>=Q}_Hlxa6_GM-K30FB zEx-`J?J0{zLp-0{DyhDSFAT4_n8cHN4zGO7UCC%%TKWh3JCYZNwo(Aaxa*A=_Y0sx zWxRM!gtTzLiAF}J+AiIs+<3tj%CMB3F&Cd_$Qm1S7n-yDCz-G1{wGOS;I;KGz%#eku}+ zx&Dg2Af*(DC%l#KhR&2^5{Q1)`kv*(BW$oARC7!_#L(XC9@YC~)c)I*;>sNm1E4;} z760fgHyRHu`A*cm7KS8WFsTh3d?0HH)f<-e$a6(!YAEtN2oTcnBEHXxjj9H{j6GCY zVm82NQp}Qx`u##l>3ynLOc_QaY3r`gUtp0+EURq$mYq^#3oOu9j{E#y2zTk}Zo4~M z`7)})P_$5dy>f6QEeD%q7)dr;M8bVvwgPrC*bY3?xPz@|&uHAXEPG#JMAtMxSabjS>q1}aD?m+2k}M44ta9?1-4j@t^TOVVNcY_{&&g~+Y>JG z5&mD5=9zt?#PP@PQ{zu-7WccU7*(BT?r_%dyrmKC=9#5dcJO!$k$>SaXZU3AWX4qD z?&<8I?j7>ikIQ~io<_U)lE_<8lqdIhck337*`r!M_@5#7S|?el6}6uRHJ->%-%c@SZ@KlibQ!l`T02^ff~G+T2;AVWeZ1G785`(!(jPZB!!y4;Am z&eFYlug_aEcmfxNp4%sKpK3d~{+Jz=XgV>167l_LxiVK4J^xN5adY|o)6MBw+)1-^ zAg=%7_Z|}Q#AYe5^kBX?er9hFVj#2^v~nq!dGD0@FpL%cX|u2OvDdeQa7?j+n`7%l zUEKP+q{pB72hPNgUK~UU@S702C@TC|(9n~$|BdBYQ#4DK5UDz}t3l~|m%|J~g-TC4wCD`uKXc0C-A*VZNo^yr+_TZ2h<{KXzz35@ z(5DR|aoIK%V8 z;XCEh@_g+vW)F*UQp;s!j~Ws>il5cKUlSsel2Gw=&uoE=F;=uCy-P@?qdwWXw6I9b zPI*H2P_{w{xce%be7cmcdPi8Dx_}&j8^M}u2F&nPQ13|!1*8^WiTli?HJe4#q*j70 zj{BtXpM>3#VkZz%kj!2Rt&%ay2diz*N~<;8Ra4!$iB!~Spe3oo)aF#iSsUr8{8k6} zW@n0S2JIp(a=-^ z5n=DQkLWOyi7pPHCl}p{BVZ20no(402_>y0Epef6*X?+GD2+Zo6)Y%;h}Cy4ZR>Ji z-{N4s!QM_OaRQ!;3ufN&wc_iP()RCZ zf1Hnk2^*85R5loBF2)pka20UuRNo$X!;OaYWVq~-=9XH9@08Z_=TK=9uFqSto@OT6 zikaV@I3Mgzpw5`G*C%z-NA^54c9S#V+GJYJdKuN-B$JoB@bUHcsbefe0jBDD8fW(~2L-V#`a4Anw(9`vfsg#Rhwt2cpfoQW^Rof927mZ(I_-XihO=YP| zUjPKkrEWDIVpjxxu#<81%M`m6%nJ-%TBWuKrSTtY>*WS%{D70c3Os9>s`*NAo9Sb7 zxqM_sjf2VT;HM?4cnJed2mWG7@=n~rI||+J+zHQ0bUmtv-gaxJ8|cI)+=&)0IakYd z^GWe8H=lcVOWE%h40}vEOgDo(Eo>us4a_XQ8y+rn1~KuklTa z1IXf7JIw%jkw)cagiRXK)Ej-yMT!Dt4?leB)1;mRkOhP)wfgEdUG}-e`Oz6UT!O=? zevso&-_x@9mw+|k8yvnMJJ^>M_tlIu;J?`)`Q-O4Ao;5S?!E1BFTbAa{zKPv zMBT-feh>I%5-j{LHi2QSer?u7XcTS-^WMW0Zl}|(Y}UY=B+1@j*O5_=TQ(u;Wu7q< z-Ve^4YNms408Wum*nzGCj2CB)9FVJL-o&pFiLI42tfJfkiD@<$5eB*Fxh-0|VXiuY zDbT=UFe%({S%q*F8#k%3aP(=S8<3lpja%bpIM~|lErmf4l*o=3A4Z2i-vT)9)3m{| zC;cX?Yh~Ht0s0yVf+>Jx*6kSNg5fyy(V6L#$L7)4 zg`pJmFixb4TV*M%^wbce5JvC-7Vb?&;O)413WBA97-Y_lH9)+R;eLG1h< zoY)>d5fXbD9zG@79?d~Vt-i-m5{wHHPvpV&Co%eSg!fxLSi|*dGpNskUG5@$PYc`& zw^UUr61AFxMdsYm7JLN+^maUT5c0lb*lJHLwVw_@zNzPPJ^|k@_dO5O)g4Wu+#=e< z)vYS09RIG zOC;nOGoEw}x=|XNOHX-RuW({LTdL~60lIbPMVNbtB8nBbVY@L!;0CQwEQ?`*^~NwDpp77cfMw_ z?!3pcpGXVj(zMDRN!FZs%#2A?(>*)D2to!2f6?=@Yt1un%{$icLSON;*NAkt_F`Nj zI!tlu<}Eq_<6uC&ysW+07ASTj>-7OJVy*az&Q!#U2%Iv4Vjc^4==?0!p%-)1gmOF;IDT@QUX5BJg+6R8R0o%Jx> z0Ipb?7-B0EQv2>~6HfYQs~LbP5QZY9U`_R$HSx8 zs*cLGGpDvCR~H|O8V~?|Iuu#}KG4)A9L9)t?hSNc0K-;EMM6)_2IhvzV_{MxWezZZAP9qXo8hY3g_()l!trC>S zu9^2zkHL_#6oA+Z^IOyNA1IS4Zm9)h^4F2(y`!=;+Av694V*>Pg5PNGlQ_ZY3Efclj&mQM3wG3->g%bh&ELbTbuJl_Fdo)$WHc-H&27Aa^D z*p?rLdb&Fo1)O=hS$o-oy{x0W?aI7;GpgKisyd^*OtZZQSmCVWg-EAKx--FZatW&I6rs6MO%9?3E`mQv9)89hgd+3aaaLnJq*M9(5 zI?UQ$-ihCp)Oje!Qy|yu2r#cL^sI|+eM#YVyyEm_6u%w08{DxZtGQJ?D63D(=4Q>> z!x&wO^0RRm^;$9{+BYPsPHG2Gy19%Jc4YTFkgv4T?S7p~T^1Kn;MufV>7aNXE(M8L zEtVNJXy2!##mOr8I~H!%)FZhWj9CM;$R4c7>Y7#VrgF-0|E>^Z_*I%0Y!|86tX z!(zm;)N(dX-|Br!p>U_>gU=h}Qb@Y4(#?FuP*GKQSjOG2nnqYOwuco%LueYBqB3U8~XhzzfdSTHP>< zo9oL_FBGrM7yZYrt5)9C!LZvvV|$2;`A9Uq=(DRfl=uJhhsq!A_1!+J0)){2z!=ft z!1Uwd-&{!S1y^Pv>J~9>5gZzQqqbLZ(8nHGFz}Xz7@W9m1_!tJtZv6=U zD2wVs&m;@oL2Vajz7NOHDW?l%dS+u5gjUK{5R082y%x*c;=LA(*TV)knRTE~3Zp&U zW^`z|MlmOAZ6SA)6gr2!{IvY!(!ZU>c?fRJ{%wr z`g%ABlIQw1M5<^1ZJ3g07N6ZhDd*eh+Km~A-5rH+8lx+l zREu3jjmR>W`#YPrmmW*@LZqDU97Y|ErPwBZdjD_Lw5rXYA_HnZ1tYn=SHg)Ly;q~} z)q1bR^Xz-C3ml*b(0}Pd|78Hu@ysRn%dXh>+4~YLM{|NsS8b`64|mHk zlG$%EHZeb^%PAt3((EPA&4JK|Lqen(V*SEId0kBGu3CQZx3M|(a!DD%RnGR6C@)7nuo z6v5d=@myoOfpY0EQh2hE&}(dv_VO?aOi%8)2}-{0W*;er1J=Hy(8ZnM(IZ~PJN%U}BB@Sia| zm|y#8I~aV6<1M`(zs4BB(0nQ)W#vjAMH)YEFqZjzE|NDom4ws8cC4xU8>ChSci;Pw zkXeOA=1?&O-}9G~R==U3Xw@dL0(AvzvT7||-`Y6y#|ZUiRY|75%IJJQw~SC!)9-(y z6*FHM_<>`n`ql8I{272RC<&IaWzvV@5EC3=FeknZBWY zI@Vwo`jlo%R>)vz)Q8md-Ys^8AE(u!@!G{~;A~36)M(BZ?l()lD<@Xl2 z1=EnFw)h4tR8prOmc~^s6cS&%6&{XNNdA-}bf9C7p^EMMsL^dRqH5~m1m8XRCId<| zlk-7}6L2+j>vN)RFxf5h53-b-rt|FvU%~=&jE}(BmD|ZN;qYT!Ds&vyR7+ihw3-C{ zM{#Y{r>~l89b~!vq1;W#(mC4Zu^REKLIc~m_1b95VWU=q+K!8&s)|Dno*|Ans|Eoy z%n)l}5CziWHmk-4 zh*t96J)FBXt3ZS4qI<{_C%KU z%mnRxInkhcptsvJ!q`tU(Rh~O<+ZFcH-ie0^~YMJdaZmD41I+GHP=S$=$f7rvGCrt zom}U>t7F{cCOeNOGGz5qR!^rN7+Bt4Wl{4LhkMS5E{CY|!4s3qeWs+7^j)6{{7-p# zbLFs?>N<gvQ@dPez5DYGZD^whRz=FZ`mo z5DWBts=E6IYB;e5$4aveQazD1s`g)hRa<;zue|M=)N%cM{~fT#<2c|;lZD>u#gLtG zb=kXsKnAjG@0jV-iRc;to6lgGDb7N6Ksf4`SING6WY0k zz&p3&GS|xik(%|Oui-rY$=ZYp&Nm?CdVUpBT+I%?pwIjo_1eL$MIp}aAtY_V_k=k! zhagq;fTDT;%v7hlUa;2;Os*hUOR6sQP(vVEZFg4b_N@9gmNiyt7_YEP3ta8OLJes_ zeVT%SUI5_m5VmU0xL@?kBb0FCHS2nG&kPhdN&1wCoHYA6fey+cuK&&=XP7O?A^mrQgE@_Ga|46 zYaFtOEx13*Tk3Hz>QxAX(pSf(T1z(>H%5s(WX?6l5+fElZC?#SGl`5Ypmf+9B{fdN zpI{C)9=*e+3w!{#d>mm`>dNJ!Mf7<6;lh?rg&D{89ASQub?k@-(%B1IW^9b(RSoj; zL$~l-1r~1*p|_YGdYB%AOh1&He8)DW5wy7Oqd504y=;aOM4Bkf$8v#k4DhkurhFXx_)At4-Z zWQ|hIl;`I>eMkMm{(-FXFiC%jeL_f_oZ&SP+VCN@rzj;N6jvV0S88z)?uzaSuD6h55%AVV7aJ$tZ z#5~$vFXOcb-$X0zTnhvg9s5el;h7JlG*kPXr_=O2`(}DFxx@}BoKU39NaMAMj#v%mKrv29QM##WJg94S3CGNayAT8)2ur* zWTRb;n(YBR*`Z_sxJMGw91bxQ<_~aT;ojkDx1fAl3af95xaqTqoTJ#MDtMhDa;D3m zkf~7{@?g{kn4f8=0*EPw6vSjkH5?0&lSB2fW8jcj>X>Z!_JfjWE5&pjE1lRLXFa&^ z)9CXsfBJk~?=ZdS@DcV0Y!9I~pSdo`EOpYMcXv|E+fBS8_^uwtM~qP5%HgaUDKX+OeJTg- zZBCi;)KO}YxciWmg9W4wu#~kbyT-QE)3Fq}G*QuY=)*22w=z*ceomGxg@?e(a9DEh zQ*tdn3e(%N+8%~yU&0JFiR@QOdo3gVn2ng3y*P%Rf%rde7<}{n_Ewl3`4Q z!}@$ler&A#T=kNY^HgcgtH_!-ONV`h;N1>hG3yscS}))g()bXb9Rm?&T$kyW1*6`e z6D7A15pA^Rnq7Q#%95cMt$BG2^&=^zytCSrV@h0%niS`uObT2VZ$iinxhU+o_Q!8m zy5ycvgb77gFapB|v+B0>!MLB*clF`eIvR4g;UBZ|E)ikt$fE|5=sQ2!3+fB&kXbY$ z-giVNsF$U}n7t$Lh6_Ztn*??nRJkLiX7(i@Cblb zK0J&;7ZMRcDZl_{wqDXO5@i;O3uXeUth0yNt%n&;yl6ISLdI0Q#Cvh-9^0Z>WNUA@g_+ zLpdkno?UYtdbjqEI*0WxoQP)P&<+gC&XAlAoZ=VDI(A(+0&QwqYkK~$~=eJ$>NYuPH$G@ESzV$zL-dku2t9T6S-u*7&Z|gtF=(SyszHhyT zOXuJ37CHHXS~V;q46jNJS{aP{ue|2o){MT+s~@OU!%tJaL02Xp9GWA7uzs1`?pjYl zP*rPzBoR3&z2t2|?LsgGAZay{&=a>XiX>E_Fq%9ix-f>iXspob7lJ_W+BGR2@CK5U zfVC=|^bF%0WpE<+HJe3(^hb!rGx_~Yi$vv{1&btJId+uwv99gsH15T9#&n}aR09^1 zn=L65oy*25)3Q{NB|~n}Oq~vFWS$z#A^U|lSiwPTe9e^$YgU0RC=(=@DA((xvfMP?RO ziS@&1Blq4=lX?R`W}Ah5+1b7AO&xhQ;jNOQ$JID+D#O)SpNIRkskd~YT4RXZZ%?Pm z@ylN83aZkMt9iDp%~|~3cK=DSvr=U87g||4(QQD|eRg#FBF8S7on`!7lN9Jw=M}l^ zcxdrL3UALj`TK)juh*QsxrCU^n;EAv-d4jB;^$KheXjPY-V^CuNA=q;-hV^1RK9;O zc8})A#-z+V!NDtqrQCYN($5y^4L*>?scCX6LoF2;Eog5!yNADe1maoMW*xX2l{EtRM2IDGtO^p<5+(lt4TV`t6O(i9Rgqcx9I84*8O z83%vmoELw6yAP?!DbD=%-s&X|ZKmZ!=!n*Q<4{;X;4Wv#g=n%Y6Yc;ghF6z4bxd3% z97Ixj7{MD)4;>?wh?v!g#Ilv8AxTft?SC2ZK!&yisSqWy7!#|4C;H;pfa1~VHG|QM zY|3tu7Cfo;9W{HS(q%24 zj$pbNH>3WZIi^y|m+|{b+w)e^JSlE>b#b$!rZMLcGYZ|7q?qLjLg zQKqUfUf6Vz^GUGs%`;biM_^?U;_>dA6i0U>WU#q_fZ&_-j^h&EZahI~=$lNIQVPUM zgqro6xWK;Ido7?r>VX14QEVv14O^@dUW#@|wFaFkw}KYM@hlqCNhMJQhis8ArEc+R zm90h?*9;x7DxKZcE;cECJ_>Z^L#U$ENNl+Lt}CjqSzgm&m#2O=(cs*?4ikOXRTsAKH_iI{`g}yA0kw_o}lJY&h2n$ zfhD?Vds|iR2WiD56AK?wa-6oO9J^wdICyleg*IT%!HJ&c>Bq`i@|H{(=k}X8gR3Ii zmjSihw0?62$8d_?lTi~NL*VFTDEZ1#=SV{QO-$*^c)Yc+LH9-{x+}JQzI`E(~+Uazsth68lZU zKT4{4O>e(?tyw&-4KJ5xWM13%#tg|LTU=l(xL-~l8NeHNYxzBH24z=N!~->P|CLRH zxw`q2{fIh4;KTJ6VB*C`O8GQw9i%gX|KLoigqqedwQDxC^WK` zKAG&|ju&6xjTNXKShnAmN0Nv366wsCTXPcE-a$-UUc05#+gr8@Ryaa2>O-i+CM6Kd zL?*{MOX+u#t9c)3nbK-xFvC!zTy2N9$NB0}FFhRxdlC`1HN;4qrtzOF?)R2AiVF!XAZZ1T*Mj?zg_iZgV0ml;XA-RGMV zrE;g2EGWEZJeA|Fgy@#fO|)fP>aF_SW_Od=%%1%W3Emy*RHCw>Fb|vl%CGBC5r4GX zI^Qjx(s_8y^rl!b(!NQg`w~XlegBr1PIJq_j`2NS39FsO4)&7D$5p%U8^G6_F%V+| z`qD(X>Y4T;y&o)D%nyv8v@LXf$n!l2UhRC$t)uiLtq2mRF0u22ka9e0{6pqZmBpuq z2i{;7=kIcMF#5=knm5f1{t^1UuO}ixt}L|!xOEJ^u6Des5U2f0D7$h<#dm-c!MSg2 zcPXYi6`l8_2blw-=h${k>>c@j^RX&-qwPZ?-zVvN`5WqfYBcJ-hfZTop2v0abt1uH7a_Hst;K(!iGaZW?>9c+%FiZZ<<{3^c3gToU?_( zeI9BZwm7@5JIhh>8Eu8DDuChi_YkC9VqOuQLrQogiU&|CQc5W!1zO+`7-Zp@DI_F2 zCTQ}`y~`PtQy3z2H~+&N@a|#Yu0T1 z#8NQttu#-jJl=y)z!u0mhvhVFsP0t6iF4^sk8QfBA8nns{S>2}q&@vjs> z%^+PE9S)QXpu89-$Rv?|I~3W0jDhbqcCsOmVxe15S;mZOLK`~pPb zFg{PQEQk+7i$Sjkba^-($Pt)6t`|IjrH!lqU8wW6Z1i^PUJxLtfJmi}Q> zv@f2B1~hmXus3Q%J384mu3=K&k6^XeE#yCAfQ!#X_6RE*5LJGP2MjwtRkamf;egIAID zyc1|s5A0#&!4AFKV&yHE=v!grA!AV6L>DDumB5^_lWlD_pK+TU>bvCh6+h>brI9eU zG4W}N*CNpO!l+5m%Cb4BkxZ0%S}t!BY3rcn0TPI7 zte_4<^n|PH{;CI;NLgq(EsWEH%asmS$g%K}R($s!Rt^o+25eOpfj4kZ^C*g7R=J@# zhS4Lk647j~z!#(3yxY+Blrw^tp_vI9c5L;e-=h8g^MdBkcA3HUJk$dKO-GSvM=_8j z+po1Grz5*iZD+Qlx=P@Q2Sr$y1G+o3lp=xuKWt?53 zoZ*|1F@@1x97kOf%Ux(9-7gKhxn{{&(z=Fx_uGaDT4?}W?^|^pP!ql1sq(~_iof3SzB!KdrA?tX6t^bJC)u4UXh~u468?I5< z<>^F-VCGAJpWU6NakCY zovng!a@h7iL9di2l=h7(!}!_TzH7ds@mK_22#>$_?w^(CEwme2wD1rB|sRUHQM!D4iD) z*8(-u4?d$~uK~X^IsmU#rMQxL3<|UW|{~65o-*T8N%x}cl zOB4~1cN3`)tBTL*Pya#ViJUjfGLVb;iECMWk=^)P!1oa<(DeLCpwY(NFddGe{0|H? zAE1#d8lfh2f3pRx`U7`)_1^`WZ`UNj+!)vEhMdKSqr{LdR8Zk2IQltlBh!|{y$9Z2 z4kx?ZT;U<#oga^;|9F}xP6^OhgXEZ6CdQ)uF)!f1Al5Yg{?7QDj|gBl&6Mm>*hDdR z+81MM{{HfPC9Hm4F?ofL48gyltuCNG;liJM!dyz?|M)DS^9(hPCwehee|OyT zXzkz35>`S9e)$m|olJ!{UQ|)2ec( zG}lw)9Zz0lrDjP{I&8PXUuoCi?`lAWf+MtUw7Y1Ws8G<1^6713{Y%DxagJ)(i${Ry zZ{TDNW|D?zf!9^CaYg+GOp z{F$y5H0e^pC^-LP50-OnU zOedo(f%gU@CDVOY`m_BLk)c>gGjgm=$?j>AE$~^fRcRV=v*L8ltw)PW+&8lW;}YSu z>7_DJN*B`-H@v$xaB0VjJB6vb9JbH#m@7dkX)wj|(pn$L3{N_FtWia&l^)DKg8_@9 zyoOLaoT+^I+HA_}6ya*;)nYF-QoFp^6hT^ymSa>$qVVmD=N^G*b?wq3!!W`Saoe6V z$Nr5H+K4ggd==Y&c~K*m{zJ2Rv-Asb&2Gz={Z7fo&VL*kR0=zd657h23ax=I>3@sR zdb)g~s79oz@^89OhOX-q`M=M8ephDwfzZlVBPXxjlA-`1kfdlYhk@09CA7bS-ivLR z0)?k}lxTN06mAIIr-FM{bW5akynhFBpgdG0I()X(-9Wkzi|K)oU_j zMYk~B0CCaiFR-w&QIYwts{2zfxBori?WgL9N2DneSO34N?iH$q9~F~4@YBofUx&B9 zs_q|HVFA~~q5Nx&TMONhagu=qwf%1>JVknVVHuSU`R@y%zhfxM#)mlPsTU5DmO{&%qRKPtx}**7yS zewi=)^SclGJ08zXs#I~`rPHZ=f}3`DNGL|EX<;vch-qOc$>kQ$jaa1(%wP9&qIl2j zx^hX);@fe_YAo=GB%E3sCRs+3x(lufO&zJS#WDs}+2y2;Z`Gim-$u*taNYd^?!_nl zTH8lH!7guu6)Z-e?ZLGzvDs(;*ps909{>!Ha970Zx6efJgW>H32Jw>h4U)kCGj2^p1g zGY=_bVksYTbd@)l%Ri%)R>NX%+!zRAQ?0a6a4k_r<2D)p^M|Itq>V3>nk(n3AWg6j zP!b??<%)cFa9(&H=1avS#EV+AAh0TTKl2X`}^*PiPpMLy}_73T9r0ls9#Ib z5#)wAiTQ(6kwX0aC!E&Nm{o+r=j@}c?F7)Un@iNGd{ zkqk{rZHA^!XuV-Xr2Bqt-@=RW`2ZR@bPgfqh3}r-_+IvH)zukhJa%@kQe8w};3>Mk z$`Ci%sLEGYr|tGR+1G5fc6!|7`#oX#x~a8S;`j^N4Xdd)mX@_?Fg8BvZ=S*`QO6eW z=f}24_J0MV@4r8`c=BUJ#2CmZ$}84?JlFo;AKUeKFI2ow$V!&_Tf+KZBKn_D7k_hX zKc1+Eor>8giwfVjA)*rDvzcHqL<|&&qt1+M9^MkeaA5!k?slOTf`m2H51=#xSL=>3 z$+h2bN=T!Jd?F zkF`=&kB%kERX=oNVBa`IEt-_?K*?Y2=28o@Ln}AV;ci(lnAmG+@uhKWLpjQ9yA};p z?st4``EtM7gF|nx^%!P&(0%&TGo_uD;y0?xM5&Gv{ZE<%3JpcFG5;HI=>NUye$5E} zY!bjSB!g)sB~9fqX@7abZm)!aep4o)ve!SmEPnrj19>ZfgKnFYgiY+T$nu!wU%cS_ z?XnQrQAU$<9E;_?SJ{ZoNeAm!61@}42jKo0q}gf~zAHy`WAzbnn@uwe7a1s^vqtVE zHEb6fw!xvC0jE1<+@}^cK=uG7x?#!}uyb~*oFvI{IQ5NLB9FJDECiN~n1_9-Y1^#) zQrpELR37EvdOlTukC4Vba)j0o+BmK|X5V!CG0-7=@WzZwyO(yQOge0^vG%_?w)SXcF>rT*EsSCxuO>$hOvC89j>wr02p^|JU5(7z$kdI8|j>ijf zSX%qWFcMX>l$^{UJCUi2O?AJzIcAcaTN5&XrRbzeo=#G1Z;RtvK$$L8@|6o8tU`WE zGm?+2E%p_gZVjy(S*BE4e?=4E@~yBy0$9Jtg50R-@Sc8Y<@~JMkBJ(?h-x76=GoqDI+c|yxc@SE&933V#y{>7;nj>G&PJ2~5W@To5hcu%eC z*N!Tu$?L9PJF3cmUrxoZ6e?zw2ZuO|nqBITPQ{v~ zMJ$vcH8Q4(fC)e5nm}fP8j-4C!I`q)YQ|}pWELlm`DUk(dk?`doM(~LSQ2@LdkkcE zaU{mh_Y6eEXFsL-EjI5^o5VbWuk6_DF@dPSovyTE9f`a{%JHHYF*BZf_vT`%ve$xo zt+GDS8Q-iLF&%>SEg>fS|f^S{7N!% zXMzvNfLIEoWQm0l9J5BdTG;OY+)@1%i}ySKX-Dlwx=HEoDxv)zM#W(D>P!v$LL{rFKg z1VbgH2B3}z%BbwD2(0jD53HlTlo}#R&EnbD*zA)?U}#SxGi}6g&c7#CGLc+NNm!$SO^c}hZ7(PZtqt`i@$*$81}zAk5|zmZqcV90 zmH)%uS3lI*UFqWP?g4^J2ni%WaCZo9!GddW*Wh|^C%8KYcXxMpm*5WPo_y`JU#Bvi z+qu8=&cE=!Yp?z6wH`^Vz-QKOf*2+W8rSJ$*PHgEs2|r&j?|llkbljX*``50Ws-?& z6d|Z$s!)i@9x$D(jDx6Q(I7d(&up$ic*U-OOQ--%Elx2Nr7P;IkYgb*hyg)JFPU9i z)i$*IT6Q#?!`93cr21_D?gDfBs1PZ(sfsrQv_FFVnC8 z>dVCcxi90NLv8Y*-V5myne|7D2AjZ;4>9S)7a*||h5&)Xsl85`Wc~Z;pQR4ye#bLbIsQ4`zd#8hq5q4W|07l1ln@003kS|qN%xPdv;SM{3`Pkt;4v|v z!4m4vGq~Ts*r@$|s6ASP=(hPlPwyxbh0&Dq0t})L^pan42J{k5i%iE-Y>Fg|(~OPo z4ATfh=*QFB<`Fj&H1UwMm`kQNBZIO{z${+MvM5;MlDlZ*09}~3A#e&|0wK(Hwj2R8 zrQ#GmI0^&<8%!lZK1Dbh;#vZt1w?tQfeiUs;G*-KWe6bBG{J-|Dj6@@Ac6`P45&JB zo|l5SzQZc54EU``KvJndRE0>r?McQU2+q6Yi{9?tEAw3zax_Tnp}E6SB>*QCrRvjUSX&W2?E&+A_dJB_d@xv38tk4 zZIFb;z>ix=`4mgI3S*Y5tLXg9^Lxc{CP<*fP&)Lr`T;+HMz}B+>e_BU<-7*lJ2%v~ z(F7s_X&apsW=4oUxfBjWzAunpn_wmaTs{~t4V;m{C_#m6+O3E~{4`$LPP9Z(4o&}Q zgD-S3dc1U4QcaGz49^UM1*%d3P@7%LY3}_pKN-mqlSSlR26GL~kR6NHUFLnGdnCov zIErWm5n3Bv#8QfabLI`gd5YwNe?^kjfkMobSq+ElF4u)YYcE%g@Zm|Sz(u7Oj+byR&E(@JtMCd!ONjq;1qkXGWOBU(zib5$r;QpZNLisYry&+OumZDF7UEQGWFM(#{Yk>nl{iV z%8N3Lh&K+T___}Q&A&3HNnEGly7?z&`s#~($j|RE5;Wq)kUdJ6ly*g0GxiK2P6(8w z1np6}MR`!eIgKE!X9cC0uMnDL<~ol2+$eR-a~T^B(0MH;;&gneEJZC2W1bOG4L?Fg>~d*fk) zZ(p|GstL38k*t=~=Dw^M158zEx#R6;X}f<}D*p~uP5iUz5$%|z9lz836l|IlMpX*^ znF-K?Wh^~g25)J;!BVOuiP4L+*cF0P`;CbL9HL2i;dfKf+=E9}&g74SWuajAU2Ab_ za6|!^RrQ@brg8u;KY6>txz>++a1;vlNEgYZ|M$0B3wVj33Gs&|g5|HL$De9kPe|d{ z5<&bkC7~PcFB%~RCD;axy%tHLYFh|w42EUl|2cOE@LFeZo51cLuvP!XB|^nUR}eO% z_V6zkGQex8bm@kwouS0{=Bw_PuLQ?ZF~7?`mIFH`^rVVuS{_OcXN`1H$d1sxj~5K2 zK)x*%jw(7g?lYH`0FHS!Q-Un@_mbg6-wT_2>FFe;llKf1F-WitrRf@;va7ZD24w&-VS&kBis-R_K#se|0go{e|I}jCL;OYW7?5EeE$og>|ZYR z+M4NUsKS z{OE~HGkgwmp*@o63x1d}kmW)$6)_YQvu!-y7n zBass=rZ9^xU;`t+6KA7knj5EX6}%Hy=x%D5_|@#*kSVYf%bP^o%2YninheHRz)nHJ zn8*U|OitIRc46;_Cmhm#h7aC{y-a8dhVe`jlLnJ4z?1x zfWY{-r@*(2HeYyjEa@N&Z^7@d$S)yaK+qFP053a?u)fC6Sq0He#PKddJQ_ke0!QtF zYAE)NT#+y*c6+)9tU97}S7rW~4>xNv36diDME2V#2ARR>{r22S6DHx~X&Fd!Z_7Yl+ppa;u45h}Q0I}tug9gqxDNyr~% z#1+4t3f;jfAMJ=FoS*J=6P)wI8t+e6FnFQ?0{_{{zyD`>VQc|eV8O`050hWZ4sKE2 zgwOh~5+Z~D@t^hTlzI8rs3YKQpw*3BIRA+v0SWxM2_O;{QUU{DfeSJ@p)rEfzr zog*s8`=`GHe*XUgNDhAlNT9y}NZ>~R3asF-M?g;~vX?|RSkBWILChCIB7vpXA5Eud z%UuYLjt>B-wWdm7dk-gbD6b9=RR6V{=l>Nz8g8iG8_N(1{Tt4yGEKFX2`^TfVIpr) z1=rwxh;g(59COOWa9NNQTAZD`aGChAvVg|RO$as4aA@yG{?k>R06Z1~Z9K(|@hMdu zuQX1#+mo3-`1McZe{NA{;6iTy`-Lp||Boaw-lBtQx zzAYe(G`;<`4NOggP?V(y#3_&&P{+Z|2#v?9X*VP$K;}dR@EhgH8$p=mH5jFs0tbwe zY+5DqQ%%wvjED-UYDEdmylsq#d{5PlGQX20?`H&^F3@F%#v{_{8*iHCrzfB(nF^#} z0qOJ7h_=ynvoeAUGE6X#3bRUz77q)Hi{O(PN=o2<-#5=7t{{Yd+iAuBft~j2VF@Bt z3i{pfq$d>qS?@myVaeIEvH3USibF_>s!oG z{|c6$UtxjYF!>`1jQt+GQs9rE5+j22!-OD^c(ahhFZr&JDi&TwT|QRs!+?Q)_MI@5 zu3nBeH32<<(rd}9CJJ+KZN z9ERo%$FTpOb=bDPhzB{bDt}g39UkOo1TY*H4TNG276{N3A6E^3I6AH#|4(~-EVW~SHOF6u=YQDaFD3CIR2G*t1r<^B z@>5_av4DwvQY-{eem3P06lX`}tq^-Df&}z*#8w%Q__1#Vp2%=21+Z07ScNe8m`v&~ z;eJm5goJ>G7=r{~rC{prugUUTs6T4N|Drkmt?De&8UTaJzp75T2yrN?1u)O+j~el} z625eZF(|P}6ZC?G@+>q673|)z5aNwkOoPB*)LkJcL~mW)B_D-o1OH?yLA640N^yyb zj}Q|G4s&n-Z-Y?&dmIk}8EBK~z~cy>ebf25E_6PaOUsh))%&z2jGo^wAADTmBd5BbCo=`YL z$ew$=zRJtmbFebj523003SS{7Rrk9@%qW8xW7Z;c-E=w*l z!|~1~BGGF|j?XH}8r3MqNr@v;4H2wq)DOW5a-}O^W8Pi#(EltosrqqnH%|NGV$e+Z z(+JY5Ds2xD&C15YP$tXW=%TXvS_Pw`22HgBtNN^qknCpdU}A5ot%k*%+K>LYB89WHy)4)&3}?Udfr6*K(OzFP?cf)v0x|3yP2vV>ulh)hDWzUiK@h@AToQqVLV) zF>uc6@uVJX$A8*OW? z>ObGkEZK72ZvWhW=9|v9=D8ti(J(RF>H84_GbHscQ%M1!H^y?)PvZSDJhdu-#UIi$ zzsWfS9b-p;l{Ss=mSHsJ)vY&wfa6GJ09ezhZK>_1q zCdt#JGMgH!c?c>Yl9v@#b*6X^_m&hT;{A7$;(iMe?BsAzs+uSyy(x=8SE4*uH+%VU zm@PVegl^-0FY)4es2bH4E@%e85HQ4jnxYTXfDyal}RtF$XsVLmWOTAsMsA7Y8eY+~cAynAsiUE9&qo z&r`csYLXcVAjQw{X)(#{T<0c2>YIk12yY#DjI{k>1Ot^+`b-43xgwXP>~4fc@Srxr zFg8Nvo$*TPqkVi2vE(g7f|xmPazlhR<(QtUK;2rkF+ci)^FQnvjsz4;B{LEAb9Q`9 z6uGcV%lagRSm)Rt-Q#8`$at@b$_O@8r{`hb3&S;LGGT{lrVv3N6_|HfN*E z??I0N)k)1INRg-GPLGRbjmI>?VPAbyF^FO-SNAP{XladaN(6(fzxXep4BIWOcj*HFS^;rBTajyx7uxKRE! zsF$o&IsU4H$`cDiO3Sk%;hxb@ehH&d8su3JA9%hq`@ofAQW(mm8BC9vCRudevy;(P zgBF^agh8SlRdZ;8w&NU-99;08btigOM`&8!^MSpT^<+>9x2m#yG{zjX%xEsrL+p&& z9XOWCs6#^J?N5fO9eTBEFEAC5373I;5+n`>nh0^oE%2RW-LRK(2OJsQxv5Tfv zSOm|qpfbtI(4;%jTjoSZmiDwGOcSTj46(U~b$nW&mP(!Pil2jnLhSpMDdUfHlx*U3 z9g6|&wLy{64*mMei7LcC%0j~2o1=hbRq`>+ub~?_`n3RWI$H7s(NWtN#Hw!I)yVz` zDskLOwSH!8xX``WE#2XVz75|zP>u&Zg&jVn&k=p3SB1Oj^&oqT#ANvxl`Tv|M7{+q^tILv0}lF%l;JR9xcSM4cS9Sk0u6mJ<;N>~}thoUiW2 zRe3r`755FE;UYES2<%GYHJYEMy$uUY zBD;&vKQfAhgLDjrsJ*PRv^3Uq?q4!s+_kVg5f?zo;;(C*8nJQ+rI6W)xK&?2A#@&k zT)i8uYrP%$>G}A$22qXx0VjLrOZ>EsAXndsKYAAM>1hKavc8*E_B;fhn-+JhzV}z6 zrrXmNQ5%OWzwAYH%F{L-2;`iRLp~k9cCqs&l5^Pj&1Le`)9!~c&QYhb%k;~qJzjLK z@xV7%S=i6}qH$t(_!!N=qvhT z%UtVE;8@FvL*(PiPtxncz2{?(wZ~2LwL7B(j|*$Br@6@V>+#6e4IdVa1I4=Gpx5$8)z=+vA4L)2Z8!mkZUGr`vMS%X^)t zeSGf(V^1hG@4y5PxJ2&=TFja6)<}2Wy71l@96mnyn3k#@(C6rc`yM1gK16putwKIH z`>uDYzE6GbH2YpBb6$A!-Yv8kJ-Z&~_`ZxmzO-uJIS9Py_nolle0d9eFesegA^5&h zb8f!;jvnMKzW-eS!B4Q?CvfhQRH8Qzhd(+8rbwbcF@i6_z7Ly;KO}0^L0fx)$tE6d^TFGF!5Ii1NgN@|Vjg)W9{Os* zC3Jyd9D%V2yiq2>70$sm&Y|Urp}C166SN`CYOX)-LTC3vI|)2{>VpfM!}0=y%EUt( z_QS^ez5LXIYQfR)biwuY-yj*l6+H}8Vz6aQu2g}|TSswK}xojc1q9IBKAujl1Ob#utYEp^vRn%iEbmDJ31YTGWO{}85kOAG5Lv;E}!?ixIHj&+#zE#0zVT;CdJ>AuHA>DM5Vs3 z#E%e2A8bg6FYr*7;MC)^40}zfK8F@bqeRkZmY^)e(db}DjpG|2!Sle=xj=PD(i*0y>uu0i=hl zq++-zT<|i3q*KIanVmJCEgZ~1xoOHT$C<}*kWqv1!KuI z2my92G2{dSRDhU9ie2bFF}WfAPi(lr)!CjvQq2?Nhgu66mH2lQbIt{ z2N=^tMa0b62XsRs7aSs-t{a|G3>-_nWRYunBCY~YJ)TM+?h$JNIYSZ`RoUbMUJX<% zw|$uo8^uR9r5g;`kmQ22^YUH2EZNV7dU_RzfUJQe9zbKEavt}GtMUlU3izW!Mx>(e z#wnSBDM9B&Q$}&C*u||dQn%O%DLuk?RH>kYlx8GZ9T#4PrK;vZeB}Y+KF~vMhpWcd z;9};$>Z;t5K1upqJYcW@5b7^T%+` zky zjU&HQxWnf0t-AflbM5+&+EpIt0B>J6C+TMK=uB`;x@N{xLyo(}Wid2`RIx3((t)Ix zP(FF3NOG9Tg;TfiI?Y=QX${g_{TOG~prUN{RdA6MbR}QzjR0(MfkuTo%42yQ4R6X~ zfoxP(K?>f>I&xM99DZu2XDXZt2D}${zqeMupL>5mYJU)@xj)#wqj(+J4_}fj#ru_T zeqw%Q#%&fHj&r4-7{4iU_S@|1q}cSy;!`P$%EL+#E}TYJJ`wKL;E8tG%m(NQxXwm7 z)OdyoN%|+7c7_MZu4D`j#t!KX+IklIL}{)myt&+?u6g+li)IrCdESe4Yu{6H=^OEy)S#qFl<*oJxDBjO z8oRkL2D#)%U>lvn@PHL?rsVzJCF4HBok(eLw%#3>E#qj+6W>YT5l{M1S|$j?Cvcu8 z2t7J%bq7j5SW%@pD>w6`b>Ia?jN6w?)T!M93)y zJswmpycW5U;{00Sbb?WGWv3;(p$l(QSVFDWLG(^=rcKtR{RMfv<3XX2cIXP5$c`L| zA$8b}AK5Jf$+jIGSb=2Lj&7fUWLAL;B>!awiR(URr`utg0oPnHXV;Ew&W}uxiqyP5 z%i=y>EsqS$fGb?@0EBl)FS(J%`Zh0g5l-{53+o2b=)re$=eQ+`9EU9)*APDFm`NI* zY9z%P2=g|V>1KB07xi*&3!b?estuPNXw)}aHmupHuV8*bEG__Xk(xo@##ece5uR7fG2oiW=Ze40n*lnj+mV+} zR)Fou9%*o{7;Adu@W658qPNKRD{~8Ja6pIwUDXk7%~%c%0WugV%vnAZZE*Ow3*WU% z;*$%>Cj&v(R}I1S&ap{hTe=?}NScFr??w2Sh+7eF;xsMdk}t*W?GipP!aH-Q%VG+hNW0%$D84+0kgK_t7a|8-Yv+dX>ipT6LxQr?Epyl$Ez;P z$UfkYHHvNqK5i$SZm1*pC|y2#QW3O553h5n*{u-lvtK*BDzR_HI32+)^ssWv-&!uB z<*gG@uXmR+A!4iRx65=lU-Av@2?rgFj+=h!%xibFB^3bee&YcO~HVv+5bj8ej96Rl>bo8)db*_*Mj*=9vigyij zq|cvZvXcz{8qi!EAVG`j_(K|L`n|;LR{{K;;-`IvQF!>hb48KT2;>G?!0Gh}w7EIEuFnNCkez;ft+?Ep5){CF4kARyl?j^PsX{2s$fYRcn+~sI^>a^(R(6^$8=)#3xh$+kIKEN>X*AfHy$rIQjM$q##EUfqLvtz zX0xU0T{%k^WTuNXpf5Ky&kNU9tJXCJe)XU07uLJ`@Mu#=?k~1Rd{l=?zdYC&&IA(0eEVAGy&9)(|}F{imSK6$h0Jtf+!7_)&jWfTNNVs zbDFH8_1iI(q66C9tbL0X+csG^ZZHbW^V~!gY^0HXtm5RvOy^z50u~D0*tveT^-YO_ zh2nsHaN-1uo!dKUto*Pl>32PkpC#B~t3pR0_8InNK5T_mXLIeuDh{#SGFi$Hn_f+g zeW0zjW9CI~c?XgwoKgacL81;~kB+W};ZO*(coa?$c(Gx31G#Xgx*5d$fpS5Vxj2)M z_=32Ks;XJ;Ia}szPO|yTf zEFKhO8YP3w3X9XZ%uDK88_kQ`W*3htI>2t9l|y8oEP->pO_tSbDof_Bvrz0S)AKe? zq&8P7#@4^M!arNDX|z&1p!u55x?#~t&N@lOnr*tMbx3S`==+||2iVKqFZ(&ip6n)A z;9IUH2tTG>O)-mkIE*O=wp`Dv=RZ3vSeLh4_B?W`zBh2$A~ehJPjJ1<2^S-B$&CA$ z?z|Vt!uzlnlR)-xkWilfaFkNl>UPr7=jnFbM$6}Z*1+=0<8tbim&ff)VB7OuQo^gp z&$}N+tfuFE@GrVR)}#>6`20|4mx$- zaneo&;W^mJ-i8@-zb2U57vY$+iEtR!J>I74O^twQ4MW}OaGT(#sW3K1(-qRuiT`g>rkLg}N?th#(ZYlS?eXnPI`WeIJ!jkkz%UFm) zGP0#In+yn~5@wd@jr5F--!7q&?l2v7ss%=0(;ZNEG$_VQV&$$|m@J&1MJ-Ndyx6A!mx-0V8kIAmqsRnt`^3GHryXm7#d7`!;$OfaA<5Ng zGCcI+Um8Q=CZh|fm}x&{7AZS@i7RF!HI+ypnyPM8%IEDdl^G+Ns*hGKp+#i?5w0B$ z);2Qwk#rv$qN@r{txFqMFRG9E!O1qJ+v5tQ88Rnv%}EyFWW9FC`Lv}xG*>i}!FzHb z?;y2g-FlPlg<{B1h&Wjy2n6!V>`2rdOgT<`h&!d)dv(4$)wCHN=g3o}qbU_>seK(4 z;Ba7Dio9^pdKozv$H64fHfH&xRO8G2=wd`{8IRqbki_H2eARb6%`foOdq|2Z z?(b7ytr+>pd>UD$u_xJ;1wtk2v^Q$%UOtkhRB7RaL#iaH~l zYF6RTW1)|~G~XxcSQY+k9^9xgJJV(IvZ^S9sq(41^fIXiJ>kS$C}N55=?0;O`4qY6 z^c&CRhlVj`PJ8&W6@Rxn(lG+-mI=+(+kjjA%9futN6gDFH^j!AQyCzJ5C%>L9GjrH z`fvUpNkY)&IJybn*aV6#Z(_GO_prXOF%T_>{FGpAZMicQ?2VeD?jt4ooc<{)h1-Ey z?wh-TFqqc=~aPZ z%YuBH>rADUeK9ig;{GI^*yEgKmAT4_@lU6PiLx7DO3R74)2F2!GN;Bap7oE=ma9V3 zND606DQQ=qSRwiD+VHi{0?^$z5xtyy8OhE=o!qx5`0o2uwJ+i$-FNA|Tt>~wE>mvZ z_XPMJ#uBuz^3XjFCA?f`FT-#?C`zrYP7!!buin)4d7L`5xvk!UR&U$jJw+;9a*8Nw6X}s<1)Nh^MViegSr1E5E(xkqDJ z`17#{14<9dfC$S0*4qJL9h6?qW`50Ok$Qq&B@5BvZ#^c>n3AbM#_s%PIVeWcqC%L0 zdgoX?dcn#RSkc}BpG*ZLfe{~$dmHM7b%lGK{rX&h2OJsfc*E=Abo*~}aABwZsuT)*Br^8`O@ zKQik#GG`w$&xEkxH?q(@vNSDoIwZ5YJhGT019C%v*q|ESV3Xa*37yj#*qGqTV;_B=ecHay}ZHN^E4TWJymMLiyNC3349sn6C2fIx+F7@jbZaYT^^ zau0{S9arF%5mb-dXTpE#BsUr12$!03C05)+N_DQ-P$v|m^M0*Kcfl=IP zbg}J;5%MAA@HiA|=~gDGmG<$*?!Ke_AP~IGPy~*QGNj@e`6P9BB!|d&D3v0LaO4{Y zq4zC=_fLvaoRZeueO!8zY;g)4rGtg;60u?7*e9JNB0;z(;dF>t?|XXqEvKJT6jdq* z+ruP;MFw(?m1L+tFC}7(O3T!*EBfime6NsEI~jw;L={&akd{#Ja2Nm1^toetQu#`3 z0HyyrcC>k1_S{{LGkAu=LV4J^4+%9!XMF@JZPru(D90kF$6Gl&1^BKVc z6~a1hmN{zHXMKv>Q1yIVMOSO4&P3%YR(Yve?(j(7LJ84^8d39k)CN}Wd3t0jM`;!? z`~5`0LU}%@U&SIE(_^LGpL#A@Z62g6FrVu@=cf~&TsH5=hEKfbQ&5Ic@wF@C@? zdkUr@qY6c&p>U8)K~v+hdZ?OMw)?tAM4hPweTlRi!AWjWOzD~8nx3!ywm_jJ{mnf% z3U@N^dghBEx{ZOxqIY+8c$n{se8#ktJk>}U^#mSiY&+E?I<D(}*6}||E6=C2CVbpYe`${S5Y7Eoh zVea4v^I)-(c!IPN9-{UzbNrsjFlmq63)|X-$l9gy+LesXE%+3aj@Hi1Fv8A0w9NP$ z3}7jbJ>zO#GiiC`NVHK8$dw9Q*{^vSu3amy!x*i@?v90ps8>TQzRz7hPSiz?S4SyZ zM^-@q%r8LCEG&cH2h6H1Z|8`4lrd?7vC%d@ZmgqtZlDHi5XS3e;vy_6hwk?9rl!T7 zFV7W}wiZ%{7lreli%4D*=mp7azGKrTi4g6-+F;~V$Jo(%yk65L6`<^Gn)X;3y55w{ zkq<_gt$bc+67E-~02@M{pnYDyxgyGbA8lsMy!Q&eTuQv#u<)(C}e};GC%wmeZ z1hUjZP*&GFku!)uu^u%TK`^du*O=x|3;Z@; z%VB()80t_aN+pVM=7A|`6#Si3^~a8p)9CD=xT;!@8aU^&;q9*e&6rOdvi0e<-wnz# z1Y$t%%pt)nWBO;%=lF<2;6794ochSdy}h#js#-*En2mPm0FUAJ{$5OgUZ9AnL#dvU zwrN1cY(QyW%b7~>n+?3Oy$_!eUC}n#Vdrg2W$lejL*|WZ8c#T@e+G*i~cblf6wfwS0Mm zxH6R_Su`_cvq%TSbRLZKRnv?cQ+FEGDDR*~71OLTb1!X#V4l4hX0t>qb7I8(2)x}w zvZEog-ICdTH+GZaynTc(>KM$z6RX>gachCAfKZha@ePZS_?ezoi})HjV%8nyQQa6C z`PrTkf&f!cZmWue$1wpOYMUqSrd5=_yR7>gy_j#NdA;TdqbJSrr>g;0I=vACo+kx2 z$HnQE3%eFR>_-cp$`;y)Wv$48HwXc6Vf(^ABl)ebR}U>jRnDa4BGG=rmzfP)tv^L* zMX5~SnyU0Us)p;%pVL{a*RFRtE+4$LQnom2;|+eJ7qKpD42?d8gdPD^zJA|s1u#-Z z=hMbmGx_#mf6NH+vKHA(<|pn?gu4Jad~}pM1M7sKwdoI^pZrfq@{h6B&W~?wB5vl< z*EXYjryOc5`AFimR{NUI%&F@_;oFdRY7LJZqqQSc@@K^hX5&;uR^3Ta37mcg;9XuA zoD+P!1m(^km&@9bI0=DdLv3G%VRw#jBQ&IDFMxTPCYq-8 zk{5SDA>W1s_Ik8RPcqVB$nY8=-!4_#7C&iA?1zxpM-J&doA+TqHB{}S{k3X7M0(u_ za+W*L=}0QIeHAMiCVSE`ue~vCb0kO&x9GdE+Pks#a;R8xkVUYlA^si3>=Il19G^p-?;tYNV2E=BKY$Gig6JzaTzjPz2Ck)hRu zow&QZ47xg6xxEu>=C@&vueFhL{?R7?UDb)F*&)=>Y1wGcC&5;Y@F9=y;T-l%*BXcD z%Ndu<)-WtUr3Klt*W`8U?XVCvpz6Q1hb-DjsJlqVg zthRgHI$py|FumzCeveksqgQUhJ{!h&BFC&Ob;8y^U3FYmrzYg_J}FSB#IET~^s#OO z<0lI9I%MLTcO9SJr`9cV<*%M^z^bS1?!$x=mkR1%GBekq@bCaG+{i$H>V8HXo@(RO zOZSN`WAF!KWrTUk=vQFzEbI?lE4tsx_bRwX<1Bx^ib4V%HXKc^9WKZkTRIjU3F=&R zoK?r(x6UR|0Cv4nKyHD& zn;hjYo$h2|Ts+m3Vq8ZvVD5%~o|LS=E@1?;Y#}Y9wcO0=hbFg9PgygUTw(FEl)%WN zGe6f$hqf)@Eu%4CJ#CKlzmGhYYG=c~8XCItPN60Rnk-oh+>CdAYkC3afy{q7doO;Jq>!r%$$b2*#ad89`4!30~LLUCfzyL4rcJUNJ3QVd6Qpyh~ z5z9W;>X~a^3_pL+{q5W1> z=FQ2K!qO(5XoZJAvLzf7!Np`i<-kGs7Y8Xa7+N(Nr?gP*n=|n25|YuaghBtocMcA5 zQ^2P5^!pj*M%(O8*@r?|b6CV>5M2QPALUA;$R0v-fCEu8q_APpsMGxc^Ot{U7G6`YOq!@qfuGp4`ShUQTH;* zya`TiR71lC$H&{CP&Q+K%^#%eqC@Jo>PP&Y;VmRM0Br1c{zBi#0bsMndU#ZJhyV`#hVv-#1?a#4V z@HK+_bpLB4{|lV~Kp4%`AWEF9&>&j+&4EFTJTJXrtdgv$VVs(Fp<%q1)j_VmxE7re z*4KE6-6W$ta1NhY?Z9q|RWIURs_m@AUYg@>@Lsy>&A=Z$Bz*BC_cH_Vh-jb?A)?x| zLq9y2Kt*vOnL>qWNYdv;YbPJ%#Q6>u=4HAr9_0NjH8mNqRS9apsB9UWD+(1adUHNRslsT};kcv9WZMSNV-O&Pjh8*F=IS-0-V zXjQ)*VQ$s1pI&U$Xv%v?mz3Pc1KJ0~Ej&j`&Uykjod}6L0*GRDhPE*PN$0dlrT#bu;8o8R36hpn9lF{L*nNfF= zshL*(`dl+B32j+BX7Z!danYju#BqtU$^G3h%g{y`A>UI)!%3O9+t+n&#v?5U(=Q9&z9}YCL$s7+$%1^oG zD`H3=_e;m#Je=gsjhgQ@yRAO{Y=uXEIdi|&futqaTU+;QrF;rnWs{IWZt9qY}R zAB{8x(m+oe9^9E1RQC_$=Q%#Gav=aBX9LBupo{qQZ6KmzCI(2Aqni@zJjmoC1Lk9a zF!CE#KWUf^?EL+123tUgDo`K0+F1mP@;p=&+gXb~dQnyHX2qopMqGR*JUX#2Z7?f(Piqj9?YhNDel%w-Y#OYU&~`|4njjEC9OkLP7t{z8dy^V-vN zgwlLL><4CNE*3-uabKAJ?Qw^QskFnA0?{RxiHKyo?50@br5t`FP2v7m^TGRqs)O?J zWR)59`qRVx7!-c<-*F9Ge@Md)PI=v2nO;}r$=@SR$vm4EKN{m>0i0bUip@jq;JJ^5lGvLWLK3T#+~2TBK96UO{n4Xs)n? zw=9iWsEwjTY}&EH5|qlDF;mGZ_b(6FEmnVwuZ@;qpP`|Aj4bxutNenhDOX#e#k_r6 zBOBN0{PVGBn?{CQ903A#4_R|V5(21D(L_3q&F_Z&RJl#ZrbtbO<#^0Yjl)k&EO0asH#o=C*Ql`l>z-3JZ}wF%d}*Ok=jkc zS2w|iGAYYExwZ0M@dY%EDG+b#d~u1ePE7{L5ecvpP)_74O|pkJxG=Y6F?FrJy{GbV z<;Slx*sOF#1qP$s<2AmYO~UyAsds0kkFr=}?J_{MS4xCVD;55MO*eWqrn&ve&RPt{b-|8UQJ*;jpIg(qv>zTR-epA@ zR!*J1B1_A$1Avo_Ms;l%d=ja1IbXjEliL%MWE{`93by6)(NU1(9QUIq0b zg}a0CClCzeT7Mx->t703L8hQ6Isi{y$530gO^|apKP0gh_z@~@J4rw1b(uf49nx+d z0G;%F?mKTztg8C#I3Ix+*ra~f?d3oEYEmcS2xdZoqC$ZQA?gjKK)RwptfTm1?}VI3 zfkf<&s6g>imM}o`A8Ga%!_dnxAYSm-gO4}(H2W>7&j-pM0An8jofm*LM2(K)mGe8` zd>9=M%QySlk1jQU%`Bk6Adu#WuoS2TV^2cp7Q}Dt32HV`3!zQ^r5zT?{1X}qzf3K{Nxex)?Z5mj%8bb!oa(=5N_}H$;~S=s+~WK2Ni7{N zRPxrBO9zcu1LeMCpi65FDi`TqKTo*tV&E!*Vd_D8$-KLb8F~_E40X1IO#PfjcK>&M#vXv3|6JMx#TNxM6ghL)0lHExSteS#OYd zOOO>sjEq`L({)fuYH(dxh%Q{X)^Y?&ON4P)|C^Jx1Vp9#6bKRLoN3_EX!;RIC--90 z(2peb1h@~nCF2$c6qk7OjQFGpM%Ow74*!mpLy!99s8nN+TsA<}tfH-S?}$$cvn-;> zx1IU`lXehBt(gpH#8g-V!N3bxrnlObV9W14@*Tk?T0H_0O3g&2$OJfb{ z4Nm?2AKfB+*%P3c9DQ{rrU^EdE;ba%nknALpuo%wxQ}MX#hj=qHY9ka~ ze^}i^38TP+(Acn067=}PrFhB)+xiv#pe2pjJ1W}ZoX7Co^ks`&He(4rdlW3&lLbv; z(ZqC2m|Pf>jAbfRyU+l3T{#o08c_B(NnmUc@RS7g!OaYk*_&SscBcrwm(B7D6M&Wr z0|9WYR1ZPD%7=!~0u^ZvDQkl695>7SnwKL!`%_A^!KC};Rv8!U7MUp2>IjRP2um1| zy0Bh@(w5v4N6U1Jk`e<2<8cdc*c)GR9 zU(v{QcFAERXEoBzJ@xuLiXZ4|(u0iM>2sQsoM>nYR{V;@Be-c`jNqN1{QV4%D_C%2 z8PYVEpAz6uokLuS49zlREGiputK8~cq(dnzRVgG9w94*v*Pp6>CFN77Ge4F15I^zm zb9?S0aOEiazT_J_l&Cb4rMmC`xNWQ6@d#HM%m=_%zpvJ$8CGmjRqVp0!+KW0MAjzY zrbD;$fS+pth;@d0S^kt7mKks z3f$BeLsNhG>yWcxn8}i7q^N1SSZ|E0+beF^rJ%0dQ#1 z3ac;iZ2QQrn;w@7z^tf$sLr8Isa~zE*pq=>%|&B($ywESgmBgYlTN>e09*B z!PFmu*N|n?uL6e{+l}55toPG~8HBKoj_in?~l{ zwD9PzM&`r0IfE^B35m~T9dw^GHBcZVsCkiidlWR>X70L$5iB1i+M^fLh1JbhEsL`j z^v{&D$oAC*gc8LOIy7@NLr%K%XtjkS3WnD7{gYifjzCrvt{P1)G%z)q4^&0L^+$+N zs3OVN6~=t94IO<{C`p9&54w(&-In=^O_ZrY@KFh9O#z6WAC-*LjIN;y7aj|zAH8M# z*a}7{9m(YSVj%+z&>-QHAwP#iV=-ri&>TOLDpSawii$>yPaX26!rYOakH4!AG-`NG zs(GYF{J{i`KJ6o*fu@lCk#ErTcCSOpTB9~F1MD5OHngDZ%)#`Dq1?>+RIkxutuZ%) zc0AeDZ6)u^PI#M6a=?sIlVH`hv>wyou3{_0jf@(VTUm7Ojb9uZaeN@uX=N}2DmJ*_gn6`R(XQ% zf_sO(82Z;G=4l~@J$oG340g$PI$X%tWVe}oQwbwyftgygw^KAlcbW9VmGz&=j ziCzMpg%xeYVcli*M7jIk>IF@SOUuPP^#yaH*Th1#_NEL2P@Pzl1woSm-R#7kSg5B- zGI+c+v$%+rKzE^{PbaxhL{(Ek-eoG8(wLy=09R|#Gg}zX>@!l_3}*g#60tX|;a`wL zBZX!alV;ytXS_=0aeZ6wXcs&Rj2U*fs93*j$*}aXv2040E!^acOM$|rPOV9Y015=s zfuXpB(S=CSkn^1Gv$~tL6B4EV>~8Df)1)|!{@;xDIjR29i3wo6K~3;iH+G$*v((0Y zQ_T|%+F_lnNWhvA&|MKl<#P|Zabq}o(=+rfQnvb*-idyUk)-dc*^iC|IK z)rI+O;1Iv5dg5Z~XjW@)Qj3}PT1I%gX=gFT?KlkRw;y6aW`0wk#CNYqepC14-sR2P z&vPZ{t_F}b8wfSq9nNTagC}%N*@cBJnTrD+IRh@empe1u>xt=HX8F<+R|Go;>y*PX z^Me>GX)V8w+nB?UmKYe;exFwBWA1u8)=YZsG6_wRbnTLE$lUO5efu)e`ML}JWv}ZL z^kZsQvN{qNy~7lO`&3_JTF#=zgxo3Pd%MfA=+_zGK@tFVl>!FSj4A80Go#e85=VSSFO*RU<< z`eoxz3w(For}EAAFhy{!M91yt;ky3Pj3MQnO{~|?Y3|02(lN{4FzaO=pWa_DO^9lC*Q?l+8F!rhH+e;0&A-(3gDd#!o*K@(bX_w7mo_H^D;7w8mQ*sG%He4?xm!)Dn5hvNw!lQZ zw^FM&pDYi4QL#~PvpH!RjAUoj?DT$m*T&~O*X|BDTos|4-`hzbPoRi%m??)YQ=pO( zCmEzRoQnS83j3z+WcIs2CPmbG)p*>WPu^b*zksWNfmL1)G|98E^b*Xal`}M_I zm;M8+PbTN}&Zs$U=(|?i`O$m{e)swMv&-o^$n_Ig+spBKVv)hS(W^>Lru+Q7u6BygNb^rn0jH#Lf zFO1L%{a-jH>?iG>jJ#RK7ccoR?I+7n~P$ zf`3RU8>)-ks~IO4tB}g`?<>v83VEw2$yF7xuUb~@tE^d-1Y>Dg^<&zqJ7$PHXgSYK zUaK0|24gSlyALjCd+d6vZg>QCs_J^ZOg2|Ke=N-8e7Uu& z8e(6qvr&5Fk1*v%u)}eX|3Pb^^R27hrdNj2`?lv7|C{Ziu5RZ}pU|gHmsQ(XW`{qt zOPTf)-WOTd>z_`SL1H;yI0PvhW$I&xE~m?;Rum|?Trw6abLmTx119XCRv8-?^bGh*6V5>{S%>7g%tK*Q?uAxam!`R_ ziy%|pV0c;g#kuT<0aLy^b7}MQxg0RGnE(uptjCx6+)pBAf|%*!|B0x4OyEI$MA_5- z5oN>t7y63-KvYmkfd6Hb{okGT$3Z0%_;-lZX0Br3EW<|W&x zT7YiBKYn0qRTd368;E)lA`}R?LaSAhV5OR~jfu%*tf5K%ZVtVMnLthb7}ou9n17T$4zjL2?`Y zTeD|_EC}rI*;1>yXG60w3N=m(aM_{+!z4G^+3uPK=Qj=pF$hE?x8`t|tA8zeO|H=- z8F*Xuxeyq3YbGG{*4eV{RSW{@_zG~c!=kl1i)IO^dmnySMok*Sq0J=(GOZ=z{0VF` zZWl5=fD6LO+76(Ur! zLVy!yp@^sC*MXF{I85lCSS1y6<9GypkbEGPz59NOUP$Wvy%G%I5PIX z98uZ8(5M|?hk?dDTNadr1bkn!s+)l|j{H&hq5q_#i)ww zTjEDVKDivE{HcXEMBsXQHB5AfYd1ocVs$-ARZ@99M%R3LJAJ+}vi?fFCtud9)7HN3~-L;wy*s|3O~Z97tP@4Hz;-_E-g$L_b=-<~x; z?luyT-ygSKeSMyeeHr;)j)sxJuP003uD8FNHNNk!?VH_CFE=>=h{x`GR69Rd>D<@L z&>k4rOFtAPqPM2z4g`ve0IUfjSdP-(FAkT1gh<42VpM(mb(4-%(Ao$hC4E@+m%(|F z+X%)-f_Qy)A)J-^Uu;YJ39m0hy+}XeC8>YMOQO;wXep%w6cM%VR)K^sJz|5@8g&tB z9r;)dBZKrFb&-0n`8fUJL(J)QQD(dacyl8|Y;ASXc3uUTY2sp(t93DM9R-9pBf~!s zu7da)^N3tIM+DHW<3gm#P*7XNMX>7Q2QbuJoInBP1$;Pc$AKvy3Ga#uSr^6H^$hsijTvCgQsqi)o{eA$Z1S zv#)PU_&)zV$>vgsgp~D2oS;x7N0`lLzbliDwh+l1lc5BLIO#HLPXc;y>-Fx+e~tZB z9kjr5(8h#A+bTC}8&j@>6hJ73s#x)pRLxrjRX6`K<)2;s!lDKPhbn}It1%0!2d^iYJ5c{Zmwv6|H9WFuK~= zbPuYIrnfSsU1I^eFx8o$i>v)a^rJ5wr#ngi444-|SO6JFqMcMJ$E~F}lQ&`NDx}yB z7)MSBS2oy@3u%l0gzZNV&>FT5cD|6*=}7*FC9}5me~$lgum^qZ`a#$EmxQsRwBqZw z=2G3M6l_a8G3%ppieXZFHGPk2$L61bws+6vwq+7^11>dP-bnQaS>+mCKoN->HwZ_)g6y<4~ zjq_Ulc*3n-WWtf?rV!NCdaneM)#C@X6hPBr7$B(0*00bg{P<=|(sR`@L%2W({V+sA4#V`C-96tbL zlXj=PO>6_xm*zGap7XHYmh~gP=C;vJ8^?x>b?0qz*0e!7^);SR4U~SozTA-SrT&}m zu#ep@KQ8^ywYK2+TlzjvT?X**BAJ`D^fUnc|8%Mu6r`ouvdHE|Clv4QZ$Fv|zF);B z6z>s&ms*@L)>GD;+ot2pRVE;q997pk7C0%L5mEi1Q3QBDQ-`(8kPog&Dk_9GgYpT~?tNzG z+rGXl`_Q`9d2U_PzHvc##iT^w!idGp4JF6ZxzW}XC*fs^Rr4sp)Ac*f_Z1roMMMGV z?OISZkY-WuX`+N=$KuiJfce6GrULRdpR%)Cs^&S*(0@${1DOB<wps6@M3v+bpsUoHueqst}$F>f85`-sgs`V47$8PwEqpFYV$` zA8$vwx66y(=lUa`frmJsTitI@m#Urv5L+GFq#RW}=t!-v8VsFry{u@gxQp@73%_-H zNg+bmcoX}WTux%p#IIpT8q8njB@x$}{}~yvU_x}rMR5G61yCY>jFPa8Aa150tfs&U;sja{A$AJz>PlnS96D;4Bv(trjmT2^W1w`+7$Z^WHhR(bHzx zv+E|rfz!5wBh*DL)a}UA%`McmRM%D*569jg&UIbXMMs?1$W5(V!9Y=7yCqmn zVg|ONR~cTM_k@L~smZdq(Cv1}EsNP-VMj5X!(SYYe4xdxcQMLt&TZT+s)sXb4kv1q z%Ie`)R4-igDreLZRrEwyG`Jx;io+k32|*^*{SU{dGD^gDBE;mDPbEt}f3fY$;sVWc z5oWOM9f&-ZQeZDreK^B>98uz~mpz0&!LGRkEBtou5Aus$(Al5W*4ox|)F-GUM!}Xa z!4&m`oDcED0p4Ttc#U`tD*>tCL%TVEn2A7!R$<35Y(qP*@2xKOaqib(>`(Ba?s|F& zJX}uP)K1qAwt^{kqoxULX|O~Bz7$;KntFb?gT4qy7Q|Hkychu-Vw}pY{;t0qa}Scf zhHWv#ept%hf9tYmGCX6=BvvbI~Z4$QM~dJ;Pt z@CJ-+&YrT*53;Y}b53D%ZmDy04jq(Q{SC_;)sIu&)%|}uDM7b!RJ!E)gy+KIi6d~E zc^7I@A7`Q=c%iQf%M4lS!^ZT4MRh$|;&5BpELxJ_<&z=gQ+P-3k7KkBe2rNxzi=n3#Ho%WmgNWl?sKq zixd$o*`Et|B8q+>6#h_3*2GQbluUl*D%PaQ4I2U~apy6@7l=X^(5e?r2xZX!&FgZD z>Q;(!@-T^}jBzjjbvML57FIG(S~^7~a@^xdDs&GqcLetjo)o(?Xf3%L{4pRL6?wZi_t29N)@{NFK{;B4l(Viw#DTkjyGn z(uz35%u>rEl7h-I+lq@V#M+XIC0z>T+C)7y_~HIJw#?bqNix87P+fd+bY*2 z`Rtyn^d8DaxU1U}$~!!&jnk`Z%gc}2s=tX=Um{drj~O|5SdvPXB|PUnujWOk*S!9% z0eaND;MLMR*O;-_z~R>*tVth~SD*f^J$tS_N2tR<v=kKTr>jnRl%psIEL+lbE%%9a2@IhFd2l`c|&atRaZi# z28f_D!?MA0Ag8BTL7Qj4MR@^2vy*4D>}`b`KF|eJ&*oXdg5OfrW?K22Q^{S?iV)Cd zQNc9a!irxJDb)~FU~a3%U~AFZ8`_$(nwZsy;c(K#6Io0}+n6Jn>O5FSn$bp9(N;NL znAuvAjT4t!~+{}>s=&u!K0XtV4HrYwP4t1(~efQf7z zuV|XeXq+DZXJl-t;>U1%=g4?xe}B6gSd4p^yVWcScZ;Xv=%x8DZO4ga!&fp0uWg(( z`^u7ED*O5bg$ijKYn`u_@?)!_zD4moeb_?^*sm#M*tk6v?LDCJo-gY?4GTY?H3?&` zbZOcB?3}xuXEg(sx(PeFN1eK0ExHVGy4#sUCiSsNNl<+E{bL&{P>)&?(TjUtn!m3X z#jbT7TPl6K#qZK*2lwJ$D5Q1O!DPh$wDBk}d~S8F;E|qamZ5G_l5CTEZS|lUv=3`g zTmOY^Q43NM(QRsUX)9He8u~36V^}F=>;*C*=$tzl3K=U+wj8pW7_y^_a(s;*PmP)$ ziPE7Rwmlsj2r^PaDpx>i>-#k#49=)4tY{6977RWe$zJ_MGSTJh*bT|Oi;ZZ(?oI>Cc9$t^iCuQl<@qj3=tI`P5sUXp(e33`myfu*jaTJIK^=UXgTwYhJH51%dujwf$2!&j0I24$v zUo*TZ5U`)K))sjc{2=6ea3;|p5M^ec31FVJrW?|xakQru;vi%wCZ`A{$*TG>VFWb+ zFzUHqVWVd|VPFunXHer{48TMHgPugRI2c?3`|Y2;KYq+5qe2+u&LpG4=*MMGCCsVM z@T#L=!BoM>=fVJL$<*~`G~yuKARy|WTrbxbMk3qHgxz7XAR6Fd3?wR5X3i^=*+ys9($D6q*tmz_JU;V>q8 z2v*{LtXw4gbj(U&{S2YTv}|2vZRiJsv*l_pVviFI@Qns2W~~62=JK+#A!`>6OsAPA z=iqYZN~2fhQDE0Z*M3b}U!5)t^Gu%@;loyay<6vn%!Q!+!wV4rjYouQ9jKD7huc&~ zwoidP*0YYL{hQSK_nAk(_S41%o?zo> z;)C~0alvL$!RCAC=3`a1G_DIcYZKyqv)j5Ox^f^bWRg`R{md$AYdvi%iVwwxZ#Q#l zrE+^&bLzBXTbl$o@o|7E?ZU?elryiZ$ca$+io z|DKH4{2gjMivMyvwQ)K_xIe~sHtM6m?0B}EeYVz2zTw@x;RD|YFk(L#s}rK#=H=rzT_Q_%-`=Ab9@yL_ z3L;4H-%0rz$xl;f2;6C0AglP^rINsR*WT*a-03qiBFC#CHQ}WsskLsCu;t;KPy1Fa z6L%A%WP>uidhtm6vazLm@Dv_M1t0$G6I)z-OWD^n6pyd)C#9&vbFRaGH^ppf#(y1F zE7l+clx1rj4T^neCk?-}8l`a^O>?xW`n0NLy6Q2?@ zh?qtA(c5CxgqyJMw{Twa84Mqh@t_Ds3|`v6!Rx_l_z(oBTQ6jSkwywH_r``~zlj;| zzmbCLFc|L>{U3DEZgh8)-z}!^!;C%g9nt(hX$vPi8_vGqLh5es^iLoYwC7M3vEAJY zG6QBK=4*Um!0~8t^4o$TkVE2V0v3pZ5LN8)3RJ-uX;hzo_(0>b7|{kBvBSq1D14mz zG}+6niWCAc66g$p@n{MG0Qh@{237$ zp8}q`q}kwa<34t34g{P?Nr1u8U zan28xdpg2mPv^PhL#1#`ThD#uW}2&jkld zYW=`{kQ?`6Y(iVceMly&(41^KLp7B?C?vnlUZELFT_`ljzs)2tvjvVz9Ae1KRnkWn z8p>v+YQK2gpW)gtKUyYy)Kb9k-^I%{YX^{uP>wBNGZU8wlr~t@b5&3>rgKMX1;32lpQNmM&1X zorG7?aNP%eD9^MN0$gmO<%INNMcCDiQ>;f8HAfBqJu;@0zIruCsHsu@&&Zgz%M>@d zn`{IF^H@-#wHiZbKHC-ZFc zcRZs~?JpUpK_1ynx*!*X?>wRU&KcHW?)e$SU8k=X6zQVoh`jkEG{||ig);$|2#!&H zB&8v%konP~Pdo;C_GHz)hfHL=@MCl-g6G_nbG-2M+;V+dj>P1RygjDeSx!^Ff&mSO zqOk%uR3tsKovCV?pL>bab!FJ|hf7O;Qd_h#h?Q|z#3jNQN{%I0J)G4)tJx+pF~V9~ zT^61zl*|puh+=P&!;A7KGSXv3+0i^SFY!V8Q{#93oNoCx(}v7dUoFP6BZx>#N8(2{ zGss>7iGqYX}qhM)-fuLd15f>MLWe)TEDEEN_;g|J3@#6?0TQETo&=JGQ zQ3;~w+Xc}|6T|Bs^-XL;Qx;^vn}#(6Lr3KaXGIbtnT`lzw$%m;C+5LdQ55)yl(MEL z1fay?2$N!=hATFbVE#1g?zkLu;rX5&4UF^iv6pkvCosfJNF5|DMu7v78sMQbiK53a zhu9Ssz%7IgvfW<=8%jeG@>7Wk)Z0f`3-1wQnv07*+sDRs7!Z?liAl=-1||*ck+Y>q z$l6@%>3GSulKm8AQ9R(v65gj!UXgH|42sgA(xxMiD^y-&NosD~lNN**Sw}TZNlYtf zm4t5_0)0stZZu|zZoErDEM+bG2S>mAd4lN>esVjoG%vG@bc^PtGS|Tr^9~6yK}u7Rm)hSKYCOs5^ghw5^;6u}28*0R;XpzDhoE8^>aQ$s1I2}72F>~_bBXo4|l6*Ue&DxTP- zLABmch=kHD|BGu^Q~%a|N#*=se4GEX*1G?dYN-ESbz4FUQc)#*538^dNKS2JWw39a z@k)A-3T`ABOeu`Nnr5_I?LXi@lZP+LgqaOb$b#DDQc5+ZARfdr6bv>ovv|Kr7Pt?i z_80gjLEM`L{gb>5K->w}^E2gd;)k_PLnID`I>Hy3} zWfVV?0YH^jYC--p-!cOJxH9r;)RHz5VFsbxNW88x50dPvIQ4!+(#inTGaM@?H?>Nb=b-j4P4NL^C_)bduE6Pu1&bDD6K}{YmqjNKKDJM97Daxq9zrAQCh7^LlCK^5u8ce+ps5T6+0AJH*T2*Ngv5n zPDhDKHMZPDT}*!#M_dXGT_Yh`0ZW?6JQ?I&SetXKqCq_?&2e@+Coe{SH?O2>eYc=y zT6MRm<$8AKA>>QX?j~VX2jBHMn1@}1TMYjU>l-n>f39L-yU3pnN$!VLtv4{W1f(%I z4Vqf6*RgYlA_&I zZB5PqD0D>xoaby6YkDaAvX|c%S{t*`?Hp}%KAdSn#Ft$mLI4CHhg>CJmEq|ay>C;; z+>dXkU5mcotouR>KS0_~o84=zaJ@avYBCBG&nM_6T`To%@)nKF<&?eNwfiRBo|MGC zUqiO)Jl{5BAEI9R!Aa*rVg>-97%u&HS~rCV)Z*jhFNqKm2!y`3pnmZ&H%Ix7012(g zP9H?86^W<+6Ufwu-u5LB5gveY0NZj23J$V~#Q&*e#uG3vPZZ#&SqPp;79z%rcdfw} zN0|#A5dRz$Zj*uWC83dLpmkCK(k-$2+M(D@nan=_MsY>7gq6iAPd%srW5WWwN<=~c zewKxFXM)TRx>3-!txpPVf|Hxk-x6{2_t+n}r6#hOtEe=@Ke+VR9dmEYaXqAigfL5V zWaN726Tb)%rVqqNs2OO>rFtl@REH0Y9o#1q`Y49%idUVfwcv?hws4x^v+C;A8^330 z67FMVow3lC++)S*gj zLIExjuz5En^xQRbDU!Ah_-Q`ZX3gQ3YTtWbT*KT+aypZ7qA@u zK8Pt!f%65jrISrz%#6r?b_w|@c>_0R%&pj-v{$|UJAT^N5(vcQ%AAsQvCjO8TBSBSarQmeR{bk0PdRNKx1w9t zFXJOE_qK)6YIQE{ocLS!DVM)DzQ^svyld)W03gT#9-<4vQK91<@mQ6*~5u=HLUtTl9A7Dmf~cT$g5)_zF3* zJavCZhC7k2cQUdxyEB?qC}RDnD7Cuq90w{6wzd@F{J8{15D3QXr58luL1}NQAwK1S zvo)7m??g%=#ipxvP~Ghf`u@<$q*d+otw1xDYOe*Zj`n9Q&!6)t7&tg_LD*bB0GTiV z0#!iJjWrGinurKW+z;Shz|?nL&jEE_#0WqU&?T$opm~f2xPwqa=qjhX&b)20E+hTu z%=EfWoiDsMD}t{Um)fCLATd~o{NRKkAis(Be5fh???s6r)lr}z8|~JXjDL2apj=s_ z=3-(9<@OtlKw?Z;8j>@pbuzkK1mJhuWIajuK?i}BQ6Cxfq@B>8rserjfbL-vf ztimuj-N1Bh9a9*wcSj_Eye9bxMWbaK=jf%wT*?9NCWeE8jw1@7%Ul{)>}C4dcxiMC zbe)u+f%FzZf`J=xCqh%J#hT)QrS{~4ROj z8$HI+mHsxEvEUp8N1AG0mR$s-KzV2c#{k;>Gy^hc5_>k}Lw~qaqx=w2E*k|%>oiLQWNn7hU;OuUl za-6!!dg(Y60JqKr%HHPTf8Fmb#(?Z7Q7-m1+VJ-Fn14ZVE>_L@`zxhwE`#r`!m{&3 z{jhDJ|J=FG(EG$#j%Q^l#-%B+^Iy5x@W+-{=;n)iUC=4s0D{pb?*X-MqCJ|?p3wvfN`ugvuG zd^6!)t#0Ru>9?1q@vi&El+Hs}u=~bK*Fz7u^Ddk43E@@wJnYgYAxe6+XOwzrwpgm&}$g5!s>?1yUR*HGe%HR6lo=C1(~ zBQ2qgaj>d$qz)HC*ddekFHjC}l*LQ+r)vqwN^#oSca3&+3qEvVX>sE)3k(Ylh;nsz@t9VxyYg=eWnKBag`miV-d_;j>Hb}dKtv_xw4k-uYvng}Tk zyHNn+grl#yv*%S+$i%m?qvk)VNLJ>9~OQo|Ed zV@^tAk{-Ed3j507FVQ-M^iz`If){V4DO zE`b3z0U!=!R|j%{(-Jsa6EuP&S5u<}o)QFc6NR}FMb#6<-4i9#5@SjdWJeR^pAr?* z{3)M+DiR55TnQS~30m$+n#W1nTz0>>k`>gGjogz{DdUh<;_)ToaaNM8T9a+5Q}85G z?6^`;j+33!QtU@l+(zTw%##DclY_WYL&6=E(~|U`k|N-fV^&h*MpJ>Usp)R1$?B=8 z?rDL?3DT{J|CaB3Xm<*^(u*Y$)6>%ak?%akhjXRH!e_)wq}54eG^l5^!24IUW>l?Y zbhw9F9H)4~r}menxTGWNrhusKx@6S7WQv(D9HGO5!U_7#iNb4uKE?$dG}TXUY>y>UvjFXyu% zpR=L=2K-rx*$h`NOUcbLR^J`XMUqsHEy|4|4@2S3`y7$?-~jHB1OLt_(WC<7bKi-%!@RZ6m>ibRaHo`fZwv5xF8MC`BW1Y2kEXOkwUphW^gOhb z6Wo>|Ij8yYRpqdi#nO~*I!dRE{YV{CNs%mze^yT~FU;UBm#`?y(J0HdC@)7S&sr_d zd@e62FDz0LD%Gec@u+CvuE-dpYWypk_FT~zQBlTS85>bH3oIN$z_c$Y+xb;KKvUUd zQ9e3WN&K^F7@@NLxv~PUYIU`0{kiHB1ZE(cCg=C+gO3EnN&5Nr$8ovb&My05get9Y z^(A-B)wA#|P4#_SO+`_StW(YYSj{6s?cH-t!rvOGaaB0{TG+K(__dmCygKOiT9}tw ztnu2%zbMb;s!ywR=qGiMk#!`Rb)-^iTQ1U6<0{nc_2-M|>kZX42o1wzs7?m;!eIT6 z>0q>DG#ie(2HZ6aUMXDKih5z52KsSycs~GR20%QnQHmCG!=;f`p;0NaK~WQ(*|JLY z1zpIpQS$_4wFpgeyn0y$#r+;+c7l_a)Wj1|UvVd7B_c>_EF?URtPWpmdH&uyvfYG?+W3{2(jx;s z+mbRMUn9P+6_>d|IZ`9Mf#!G`oeNt@-FrK0*E)x3 zn&}WVKFG;ogf4s3P9gi&h?ABpO|e*7^kmIeIZyOR{O(K7Zh6GkcOJA<9u(8@W;Zs$ zB(xR+|DHm&R(PbcVom(x42=%MPG;ujQmI~4s}5YP=5n641D?*!7XeF6bPvtG4oUL! z6Lfq6R5nkX|8&R$0CeyGH~=Ex)5mL^4v+xA5l8Xs@fSysjE5v52GGNOs;3s06dVYU zXo{m*j?)7&12pUjMwul9{`cp6?g!?I0#WjR`F;Z7B982^+xx^;fSo$9> z-G7$Y{y(&$@?SsajsNHD-O&Hfp7Vqhc5zV%u>Wl<>pu@EkpHq6|G#rcIhG?s{m%98 zLkgsrH3SjUPe=&jx_=!~q*Ndi;KW&mD%YCrb`_(Dnyde`yJ413jyKnAcKiMR^N>Ql zpZEb7?KM#<&}Y5xlV%EPH_j(1Ijfb3LjM5Gwi zPNoo(J61>#ZB$F!XwSR1^2agT!3>dS?WK^tXHEAm`%J1|a(L`p|4{JMc0e7rU$rjS zWY+dPp-EkIAm+Trk3EKf>boGzI%=Ee94fc_B(^GV#JFxcZZ!BGkQ%3jk*ynN#6MLz z%}TI-STf`|K7;0zl`74!A}n!RSD)8@&sgW*)TX^N;HvkTyiZ)D!8U_f+l<7O{V#$)m}M;@^d?u1%1DOT zy_}-7KC@A3(xlk%huCjv)kO4ByNLTZ{apV5#zA?6$1TG=_p=tZT2bI3*Y2NF({Xn~ zM#SJ0aZv_f+>@gb8SyB;gbENCk-0}qpEfFGgO=d$g!E19SWNL?Hqlp_oJLiBOeMSC zEKf-iLavXMaupcf(ow|VI6AKVT>me?I8b6j|33la31jYtRE(%1-4KqlO^G?G28?9< z8m>u8`mywN0!el$G#N<*i;V60(ZR)0`r5{XEDXvDHrZXso>|anE>wZd7p!igs{11!xWYBaXL#y>BnXxo)h%(?~ZuR+?jys zjfGS{$|Qe{{Z49YEMoF5lQxu`OIvL$=Ikt!bsU?^dTuNccq@|+l$_7Q1C@#^Q=_5O zvJ^L+meB^5D-}pClxeV)-&K~Y)Ql}ud4MYP-pbYbB^T?`L6v4dDm3QC7C~*GDm(8J z4MQx|dWyShu}NB;hw`Pa>$@7C&;K0Bm;2DLYG}vJ#K6MDOe4 zCh3fs+tnvE78^1_R^tMQ>iy~WwIwq27OEAii}n8kjP+B}mA$|VO*)F5)`q?1Ya0jm z&B}MycK$C)+gK?rY9iIvNs(*Y+z&0&GB!>rJbw-ZAKHEoGB|@Q|D1Zbwr`#>xDC^8 zT%WUd9DcQJP0i4`S;T6(OsewQmD+q-ZSH#PtT9JlTbX3X$~`HvH2WmI`tGyX11)Oj zk36vj&EeMj<^ISFQ(EEe;Gqv|iYbJn@*lvsR4U0*ds-3&HN$Oyimxu>*TfEHd&?k` zPhHd~%@)qLhkn8-<`~z?UBa8kVZQx~K%r+{1mvd?)R0CDdb<@Lyv5Rf$l9u-OhBR=yjg4daF47^_+SBAwXG7+d zmB|lF*G|tbw+@}sW(@dkGhuv<`N4Gi&RFhaaW+m#S<=Via;v|SlAVfi$B(3T*=KWA zoJz(1xAwj}ps74-`z8iR0%`~Wfk6xqLZ|{FqM|0$BorYcA}AugiHL|`4ZUONO_UZ& z=pbDblnx@GR2w#`B4Pn6-vvizc6Mi-+5KjB-dX3*i3#`KbAIPOr##P*ourj1H!5E! zVePsxxx2QeulP)PzLnmT;LW7lRm$asM{i8+@3?d;xo}Kv!qm!VyXmd9+uoLJs~Z8) z>IL**ql>gGT^5cVJ{RYq$}DEHKAAk!;4BF#`Au8QPV-rL?geQv;M8_2LYfM0D%hy z`K7`nSAgW9dPg0!&kAl0%%GGA_z%!HjZ{sGufP!k$iPA1m;+e}elG!7@z42D3L?MG zkHTvM%Tk}vXkhIUaNNcX7ZSsK0gkajIA|_F!@*M8f0Eym=>y2<1dtz9aL1RHbR|V+ z3BJ7gBK27;vVauU)HTbrr zi-n5Q!KT$S8qW3>Yrhi@u!yx-ccm6DcI9$U4Sbedpp+p8fCQf<9#{b*iWa9f_;Mp7 zdNgcrk#L4Ew65Ze1sz6QPZ7a+EqNM!A3_Q5V zC>*4B3BPYqXuvCHno`o2?o5rzp!Tx%=iwmkK1*hZuvVrF85wunsr7s{xvi?nxK@Ee zrVQy+6(3}gaS5@d{FIBB+Qv-S13f_I9PJhYIV}T$;$R->$<$T?Obw2_q`{VYu<#T$>PM) zr!U^T7!ePV)J%@ z9CZF{2o&^f2>A4nfcIkh1TdC4fZ)eBAuGUVz4$QwS(3rM6-Y*dV`)!v`|?c6MpaL^JJJdk<*oA!9er|kwJmZ7 z3y6i}$Qtl$O!t=A<_+KLs62e5=*YSE;35TADlULW+Pt%>vf!(we1chV=Tf55{22L*yNdjScivnK0lQpaRH&u9pOc zlEEkq3joXhqbh1Zax3N(xg@>G55Z0q@hr*E4oI$WK0jabL^G16m9&p?gp(RCW2K0Z zBr!Xap_^)Ae99yM@8lE6wN#ANZ_4L00YwishQ}eF3Ns!cGVkqJ-zCpJOB?C2vAT^b8jW%%+)ePRd9gzjl$i>0I z=u_2f0up$jF&=6pDGDIE5%gdeC^7rI$nah}sQ70DELHry*I4k0q-7g}Q78x~1p&<> zIMe`EkVT?#bdtAOg^&ipffMCxmPt19Gv1P+gZJq9|*! z2wU_{OG$bRjwZ>#Kyf6|7|AW5SD8j600074;va=CMQA`Zw~(`gm%;0Ax90yqC}{i7 zTeB;M9ztV8KH^Ju_D;u`$$MEw({L2hODce9q?&t9#41BrZASK_34;+!!jeqraM%f?k zQHDUL{bL_9P>5LoH%+iaLnGCY!RUE{B@|$YvIQd;qL5$;qf|3EnGvZ06@}7qQQA-l zJ zXbduA^frf!MwJ8y0pQ%Dq!_fMiBAlV1)@ZT2iwnyQb|YwRUd*s1MLSVf@cg2=R)4g zul#>CSN=sR{hL#8@ZAv(dWhIJJ%s�}VuXb*WD))6TR2moOOO=%h^0xE#RH;4tJ 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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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�}k*{Kb-qXcQx!=P5FBsK>&~>fzY$veCH{EZrS7;%He-F$f$>zMjic`E=uuzEv zA1;M40_2yBnNqNpjq~n1a-vCWS$2}p^L2_mnT1(&J%CS<^oceSa-`nx8e|xgEYTUbl*ptRj0JhVnVW87#^EjyXwBjrV#zk~tbNh5;6sOfgpqB>Zm5xK=Ru0o&+fyPTGyUmDeK$4$8$<& zZMSR4LcBjWM(6$aXrCKDJpr75eZnM7`Zb6np!{p-R+jBzm{`yGVuZ}D?qZbE@AP7f zHug1lL|%L$gsy30vw2Z}dSufz=4J)T=&^R$+j1XY9yAH1bAE;R^h{h$mCp-Fo4 z^2K2`=kYSR3CEh(s`)bb?|~SETwz13{p~@UyMU<^o~I}`)prRx!}@(ORh>MtHs(N# zHmFRpJ>^F`T2aWjINEl+I@Vt`)#8TqVFb`dg;xb$)Uul!$s zZW|TW!8I>r(XYGlZz9n@`F6c2mpwOtDM6{7&CdUZqO#MS2zr9^96Urk{u=q9RsxqTr=a1xXDT6N~7^#B+=}=}P&FTYcG zV)^Qn^Ae-@(>UD?Iaa!%^@M~XQu?N(6;7s4UmPNvr8T!oUt$f{6|KrvFk*V0&OZs7 zos%YJ-DA~=A86LLbK-wB%^6$GXY|w4RP-b-F(dx7q3VR4@A}?nT?k#Cf|kT~AUQ~= zw9$zbJL5Yt9R_UQnt$e0h&{Q9Vgpe1j+s#8?6wj51}4&%t>XC&PkfgLM0^ciE6)`P z%;!8fQggs=Di&Nd(Q)>oy*oM+D3WGTLC>sKVHD>)q8kBDTctL;3}{mmD~m80rc)2% zsFg`-Q)DznYf;ru5z>yx7_nl=Wh^k%%ZtP0=Wwut70Ytx4pqa_(StU?S^|CX>_5>_ zJ~ZHK)YB@!+G!&+Fi6z+RE|}q`6VvA;oc?~wYp=9OD*}=$n0n*N`^qKf|tYfL<8IC zUlSx72&J`-E#xO8v!n)R;*syQ#{4vY6dsSMl1!OoGPI`iDS_v$>GNVszG{VjYI&h# zXFO}EVd{?n^;Y-Ax$iY&`aj8T>H}0xjWLNz=VE+R5^&j@>J;)nB3$`fFt^u@LUyxd zU6(ZU!^+En44i){^Z({e^+R+gCU^`#RP_|I@<3HfoYb5wr3&Wj512nzf6&JA>n@&Y ze9`FjM%rnff^-SQUp1-HDEa!`ePM%Vt`<(xIuEgCInV?z~D`NU9y*FEB zRIa`|Wb@fwELA5Hfu1`%&3{MwCrh}GN4>hfFzru(w697g%0Axk?Wy?#cwORp9rn^< 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

jd>LBJO9-`?84?K6KN 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∓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 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 000000000..f5a913130 --- /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 000000000..f6f7fd584 --- /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 000000000..9b4efbd5c --- /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 000000000..cb3ab49d8 --- /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 000000000..9934ddcaf --- /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 000000000..71db2b8ee --- /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 9bdaee45b..eeacb36e7 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 076ae4d9d..f38b0ec75 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 03c41d674..d50c830ba 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 65fca93dc..52366bead 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 9934ddcaf..360f75e31 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 71db2b8ee..44575c6b2 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 af12d57fe..b163dfb75 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 ca17646fe..fc6cc5459 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 183d86298..839e7a400 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 75339cf72..dadc52e05 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 55b234e3a..b01ad6544 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 f4d6ef9d2..245b3ed1a 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 eeacb36e7..8d6eeee20 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 000000000..b6f306275 --- /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 000000000..53154812d --- /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 000000000..7391ec299 --- /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 000000000..a7854ad8f --- /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 000000000..62a215752 --- /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 000000000..97eaaea8c --- /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 000000000..74640bb80 --- /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 000000000..446ee51d5 --- /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 000000000..df6949c64 --- /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 000000000..d3bba6ad3 --- /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 d50c830ba..8752ae858 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 f5a913130..b1968b486 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 360f75e31..d8a7c3f6a 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 44575c6b2..799628e31 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 799628e31..571676b92 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 571676b92..79a9a6233 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 53154812d..f421a15c2 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 97eaaea8c..7e6a95950 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 df6949c64..d58a98265 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 d3bba6ad3..4e41c8aaa 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 4e41c8aaa..e8a78bf19 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 a7854ad8f..e1c8aebdb 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 7e6a95950..005ed8afb 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 74640bb80..1944416b4 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 446ee51d5..6fbe9686d 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 d58a98265..395e0c0ba 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 e8a78bf19..caa513c82 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 4e3ba2f15..f7a726a3a 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 8752ae858..6cb49a5e7 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 fc382d711..049b600ec 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 ddbb27646..000000000 --- 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 cb3ab49d8..6a49f7c4d 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 000000000..12d7058a8 --- /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 000000000..904ab104b --- /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 000000000..ef313a61a --- /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 000000000..bf3b4eb75 --- /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 b255282b2..d14f15655 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 70d70cf5f..db58d35a0 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 ef313a61a..aee38a3c4 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 d14f15655..94d1e74c8 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 d083675e2..ce4a28043 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 0a10e29bc..a47421d73 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 db58d35a0..230825a03 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 ff291e9ac..77db918fd 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 aee38a3c4..274367b3c 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 bf3b4eb75..9cf2e536e 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 f38b0ec75..93e1157a0 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 b0bd06401..230a61e3f 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 a47421d73..518b07f3e 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 77db918fd..ea678222e 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 274367b3c..7809c99d8 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 9cf2e536e..b256b1024 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 d6289be9c..9cbb99cb7 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 12d7058a8..c5f96fe26 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 000000000..2e30bc39d --- /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 000000000..90e7a8c2a --- /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 233873ef2..ee2820dbc 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 b991b135c..b70b570e7 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 789831423..c5434b39c 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 1548de988..81c2a4a2e 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 5fe732dda..d77f8dc44 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 3e8573b86..d92d43be0 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 000000000..f3566c7c4 --- /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 000000000..2c99a9258 --- /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 000000000..9440312ae --- /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 000000000..d93f7162d --- /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 000000000..b8df31978 --- /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 000000000..48c19ef07 --- /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 000000000..bd301eb44 --- /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 000000000..05c229b53 --- /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 000000000..b9faa6019 --- /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 000000000..97fdb8a90 --- /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 000000000..5230e298b --- /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 000000000..e82469680 --- /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 000000000..01ae25277 --- /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 4bdaa2b63..7b7f0bded 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 56cabdbe3..fab0406f2 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 a7bfa14f3..8730809b0 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 526af7f37..1a7ba5e41 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 2c99a9258..c1be381c4 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 9440312ae..85590e5e0 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 fab0406f2..e707dc008 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 8730809b0..4cf3efadd 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 1a7ba5e41..af95cbf2c 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 b3386074e..5b0282b61 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 000000000..78e40736f --- /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 000000000..33ed266a2 --- /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 33062425b..b1e0fbdfd 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 2f8d862fe..1a1b83aad 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 33ed266a2..7e43d3a70 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 c48b32962..a75772171 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 1a1b83aad..126127822 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 e92fac96f..116bdd744 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 7e43d3a70..034daf9be 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 74dc42b3e..04f0e2206 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 230a61e3f..dec65eb9c 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 3065d4f61..e6403d007 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 8c5f9d0ea..3c7471de7 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 034daf9be..44e1e4b27 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 141659818..1c38b37d7 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 d9750bef1..dea469e54 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 78e40736f..056c75631 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 44e1e4b27..7b5322188 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 000000000..11b62fecb --- /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 000000000..da29358b6 --- /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 000000000..6bc144ab0 --- /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 000000000..4a24b7970 --- /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 000000000..d8f3d94e8 --- /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 000000000..c775382fb --- /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 000000000..a5c821747 --- /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 000000000..4c1f557f1 --- /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 000000000..f231177b4 --- /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 000000000..1ad3d885f --- /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 000000000..4dff9afbb --- /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 000000000..a53f834f9 --- /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 000000000..46aba428a --- /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 000000000..2983b45b0 --- /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 fec75796b..13f683882 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 1ad3d885f..c2d81960e 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 46aba428a..a80f7806a 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 2983b45b0..5dae31e1d 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 e4476a113..dd7d484ad 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 b38d75221..21a7a12ab 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 b9c1fae95..a5ec243d9 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 fe0ad481c..2f2f74bce 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 3c7471de7..315d823ce 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 af3e41741..47718583b 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 907c539e7..250917253 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 000000000..626c788fe --- /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 000000000..a60414a39 --- /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 000000000..58e9ff8e2 --- /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 000000000..f9b8b37dc --- /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 000000000..3af4fa44f --- /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 000000000..7efb23b37 --- /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 7efb23b37..8b3fe0dd7 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 f2b4cf981..a77b166d6 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 21a7a12ab..59324d6f9 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 2f2f74bce..ff254b3e2 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 dd7d484ad..4fd3c8c2b 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 a77b166d6..1d46f054d 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 59324d6f9..3ed5b99e1 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 ff254b3e2..2c16d4ab7 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 a75772171..02f14fdc1 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 4bb38fad3..2acadc769 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 3ed5b99e1..11c8da690 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 2c16d4ab7..53c1b4206 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 000000000..e7713055e --- /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 000000000..4c86c8277 --- /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 000000000..2688e5a53 --- /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 000000000..5a85fa33e --- /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 000000000..2cfbc4ba8 --- /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 000000000..011cb4a38 --- /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 000000000..b69245939 --- /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 000000000..071d6d6ca --- /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 000000000..d8e418329 --- /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 000000000..e6d8cd8f2 --- /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 000000000..c185ecce6 --- /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 000000000..68e4ef06a --- /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 000000000..21c8dd124 --- /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

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¥RiY7 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