Compare commits

...
Author SHA1 Message Date
arcbtc 6cf488ea4f Fixed lnurlw bugs 2020-08-19 08:49:52 +01:00
Eneko Illarramendi e01de0eae5 chore: update requirements (but lock gevents)
`gevents` has been locked to avoid myNode installation problems.
2020-08-18 08:43:04 +02:00
ArcandGitHub 5650731bce Update index.html 2020-08-16 23:25:54 +01:00
ArcandGitHub 53d65c0258 Update index.html 2020-08-16 23:22:32 +01:00
ArcandGitHub c4db2d5c28 Update index.html 2020-08-16 23:21:44 +01:00
arcbtc b97595b06d Added "quick vouchers" option to withdraw ext 2020-08-16 22:46:06 +01:00
arcbtc dd2a282158 Fixed migration bug 2020-08-13 23:43:42 +01:00
arcbtc 3c7e4a3eaf Fixed events migration 2020-08-13 22:01:33 +01:00
arcbtc 89c31139f8 Merge remote-tracking branch 'origin/master' 2020-08-13 21:50:16 +01:00
arcbtc 2d580a0d0e Fixed lntickets migration 2020-08-13 21:49:50 +01:00
Eneko Illarramendi 0497c603c0 fix: lnurl bugfix
This is not using `$ pipenv update` because apparently other package updates break mynode installations, see #65
2020-08-13 22:19:36 +02:00
Eneko Illarramendi 5831fe2ab6 Revert "fix: requirements update, including lnurl bugfix"
This reverts commit bc984ab0db.
2020-08-13 22:16:16 +02:00
ArcandGitHub dfdb890751 Merge pull request #62 from losnappas/patch-1
Update extensions.md
2020-08-10 19:49:18 +01:00
arcbtc 3735fe7aa1 Merge remote-tracking branch 'origin/master' 2020-08-10 19:45:20 +01:00
arcbtc 97a13526d9 Made print outs bigger 2020-08-10 19:44:55 +01:00
ArcandGitHub be04623b9d Update README.md 2020-08-10 19:27:37 +01:00
arcbtc c56784340e LNtickets now works 2020-08-10 19:08:44 +01:00
arcbtc 0603e525ff Fixed LNticket extension 2020-08-10 19:06:56 +01:00
arcbtc 410e96771e Events extension works
Little bug fix
2020-08-10 15:39:04 +01:00
arcbtc 784a2dd876 Events extension now works 2020-08-10 15:27:30 +01:00
ArcandGitHub bc930ab606 Update README.md 2020-08-10 11:55:15 +01:00
ArcandGitHub f518e350a6 Update README.md 2020-08-10 11:54:28 +01:00
ArcandGitHub 8055e6a73c Update README.md 2020-08-08 21:17:07 +01:00
ArcandGitHub 6b2061f107 Update README.md 2020-08-08 21:16:46 +01:00
ArcandGitHub ce2f764432 Update README.md
Added some props to our forebears
2020-08-08 21:16:01 +01:00
losnappasandGitHub efe4b91877 Update extensions.md
Add a skeleton, better than nothing.
2020-08-07 07:37:41 +00:00
ArcandGitHub 55a9dc749a Merge pull request #56 from frennkie/fix-withdraw
fix https being stripped from QRCodes when showing print view
2020-08-04 21:19:52 +01:00
Eneko Illarramendi bc984ab0db fix: requirements update, including lnurl bugfix 2020-08-04 19:22:24 +02:00
Robert Habermann 1e36a8a393 add missing protocol (to ensure http - or more likely - https://) 2020-08-02 20:01:07 +02:00
benarc c17ac32c86 Fixed LNURLw migration 2020-07-22 20:17:37 +01:00
benarc c73fbe1b27 Fixed unique LNURLw for print 2020-07-20 22:08:54 +01:00
benarc 2ddc23de04 LNURLw fix 2020-07-20 21:32:55 +01:00
benarc 294beb2fca Updated Withdraw extension to handle unique facets 2020-07-20 21:03:23 +01:00
ArcandGitHub 6c286cd265 Merge pull request #51 from frennkie/patch-1
Update index.js to include port in links
2020-07-16 12:17:58 +01:00
frennkieandGitHub 68e41789ce Update index.js
`location.host` contains the hostname and the port while `location.hostname` is only the hostname
2020-07-12 16:24:18 +02:00
EnekoandGitHub e746dcd787 feat: enable code scanning 2020-07-06 18:51:02 +02:00
ArcandGitHub 61f736878c Update installation.md 2020-06-22 15:15:27 +01:00
ArcandGitHub f51d44e99b Update installation.md 2020-06-22 15:14:51 +01:00
benarc 95b0ba536b Prettier 2020-06-15 16:53:34 +01:00
benarc 5e81c64831 Usermanager extension update 2020-06-15 16:35:30 +01:00
benarc d56c2627f4 Endpoint for updating user extensions vie usermanager 2020-06-15 16:30:24 +01:00
Dennis ReimannandGitHub c3281399d2 Improve setup instructions and README
- README cleanups and link to documentation
- Improve dev setup instructions
- Fix js linting errors
- Move local setup instructions to installation docs
- Project link fix, closes #42
- Notes about frontend
- Clarify different LND backends
2020-06-12 20:58:37 +02:00
Eneko Illarramendi 8ea72c9934 feat: add CORS headers 2020-05-18 23:33:07 +02:00
Eneko Illarramendi aed2414f3a fix: show better errors when a LNURL-withdraw wallet cannot be created, closes #39 2020-05-18 06:33:02 +02:00
benarc 76f17d8594 Removed API docs for Support Tickets 2020-05-11 13:21:03 +01:00
benarc 85e617aae9 Updated redme 2020-05-11 13:14:25 +01:00
benarc 68d94a43d0 Support Ticket name change 2020-05-11 13:11:36 +01:00
benarc 29dc33dd71 Merge remote-tracking branch 'origin/master' 2020-05-11 12:35:29 +01:00
benarc a54bff6640 Changed extension name to Support Tickets 2020-05-11 12:34:35 +01:00
benarc d2874a90e9 Working Events extension 2020-05-11 12:34:35 +01:00
benarc c099885bbb Changed extension name to Support Tickets 2020-05-11 12:34:16 +01:00
benarc b296c38795 Working events extension 2020-05-11 12:16:26 +01:00
benarc 9af4161679 Redirect feedback 2020-05-10 18:46:58 +01:00
benarc 76510b7892 Events extension mostly functional
Can register attendees via camera
2020-05-10 18:10:45 +01:00
benarc ea3b858695 Replaced events extension with almost working vuejs one 2020-05-09 22:27:11 +01:00
Eneko Illarramendi 41277e6931 fix(paywall): disable create button when an invoice already exists 2020-05-09 22:52:30 +02:00
Eneko Illarramendi 8add56a24c feat: allow limiting the use of LNbits to some user uuids
The `LNBITS_ALLOWED_USERS` env var can be used to limit access
to the application to some uuids only. "all" gives open access.
UUIDs should be comma separated.
2020-05-09 22:30:33 +02:00
ArcandGitHub e768e4b075 Update views_api.py 2020-05-09 18:07:31 +01:00
ArcandGitHub a40823de43 Update lntxbot.py 2020-05-09 17:29:24 +01:00
EnekoandGitHub 171e639109 chore: add paywall address to github funding 2020-05-09 15:23:22 +02:00
ArcandGitHub 6f4cac0c41 Update index.html 2020-05-08 22:19:43 +01:00
ArcandGitHub 97dff38af4 Update README.md 2020-05-08 22:17:43 +01:00
ArcandGitHub 13f01dfbe6 Add files via upload 2020-05-08 21:03:18 +01:00
Eneko Illarramendi 14d61eaf56 feat(withdraw): type casting 2020-05-08 21:12:31 +02:00
Eneko Illarramendi 63b9741c14 feat(tpos): type casting 2020-05-08 21:11:59 +02:00
Eneko Illarramendi 22451ca806 fix: allow empty values (nullable) 2020-05-08 21:10:04 +02:00
Eneko Illarramendi e73a508011 feat(paywall): improved extension
- make remember cookie optional
- improve database
- improve type casting
2020-05-08 21:05:32 +02:00
ArcandGitHub 2fac47c05a Update _api_docs.html 2020-05-05 20:53:13 +01:00
ArcandGitHub b8dd151043 Updated api docs to include listing specific LNURL 2020-05-05 20:07:54 +01:00
Eneko IllarramendiandGitHub 42d307adf9 Merge pull request #38 from fiatjaf/adminkey-to-inkey
allow inkey endpoints to be accessed with adminkeys.
2020-05-05 11:09:02 +02:00
fiatjaf c3ce18c8d9 allow inkey endpoints to be accessed with adminkeys. 2020-05-05 05:46:05 -03:00
Eneko IllarramendiandGitHub d914c3f701 Merge pull request #36 from sgeisler/2020-05-domain-https
Use https in base url to avoid mixed content
2020-05-04 18:34:22 +02:00
Sebastian Geisler 52aff1128e use https base url to avoid mixed content 2020-05-04 18:25:16 +02:00
Eneko Illarramendi 9eb253fe55 fix: typo 2020-05-04 17:34:53 +02:00
Eneko Illarramendi 35fb524e1e Update CNAME 2020-05-04 17:31:53 +02:00
Eneko Illarramendi 01c8e8a23f Create CNAME 2020-05-04 17:27:49 +02:00
Eneko Illarramendi b37af0c600 feat: use github pages and jekyll for docs 2020-05-04 17:20:36 +02:00
Eneko Illarramendi a61d346669 chore: update js vendors 2020-05-04 15:33:39 +02:00
Eneko IllarramendiandGitHub 8530413537 Merge pull request #35 from sgeisler/2020-05-js-formatting-ci
Add JS formatting enforcenemt to CI
2020-05-04 14:56:43 +02:00
Sebastian Geisler 3a6034e8f6 Add prettier JS formatting enforcenemt to CI 2020-05-04 11:40:44 +02:00
Eneko IllarramendiandGitHub d9fe80d41b Merge pull request #34 from fiatjaf/prettier
apply prettier to everything.
2020-05-03 23:40:54 +02:00
fiatjaf 4730500ed7 apply prettier to everything. 2020-05-03 13:55:17 -03:00
Eneko Illarramendi 083f7a0a8d chore: use standard library's HTTP status codes 2020-05-03 15:57:05 +02:00
ArcandGitHub 3a1167c892 Rename config.json.example to config.json 2020-05-02 20:17:33 +01:00
Eneko IllarramendiandGitHub 236bcac263 Merge pull request #26 from eillarra/less_requirements
refactor: remove wallet only dependencies from core
2020-05-02 19:25:47 +02:00
Eneko Illarramendi 2c5e539795 refactor: make some wallet dependencies optional 2020-05-02 19:16:10 +02:00
ArcandGitHub 5553ed82f0 Update index.html 2020-05-01 09:48:00 +01:00
ArcandGitHub 9fadb68fed Update index.html 2020-05-01 06:28:45 +01:00
ArcandGitHub d496869572 Update _api_docs.html 2020-05-01 06:12:55 +01:00
ArcandGitHub 9f33ed49e5 Update README.md 2020-05-01 06:12:20 +01:00
ArcandGitHub ea480866ea Update README.md 2020-05-01 06:10:44 +01:00
ArcandGitHub 690b33e096 Rename config.json to config.json.example 2020-05-01 06:10:16 +01:00
ArcandGitHub d72678972d Add files via upload 2020-05-01 06:09:51 +01:00
Eneko Illarramendi 8dfdd15adb fix: self host icon fonts 2020-04-29 13:24:49 +02:00
Eneko Illarramendi 8442f277c4 tests: only use github for CI 2020-04-28 23:09:59 +02:00
Eneko IllarramendiandGitHub 8f76da66e8 Merge pull request #25 from sgeisler/2020-04-ci-lnpay
Use LNPay backend for CI
2020-04-28 08:49:30 +02:00
Sebastian Geisler f7ca28db34 Use LNPay backend for CI
As a quickfix to make travis happy we'll
use an empty LNPay wallet as backend in tests.
This should be replaced with a mock backend
later on.
2020-04-28 00:00:56 +02:00
Eneko IllarramendiandGitHub c951577962 Merge pull request #19 from frennkie/allow-lightning-scheme
Allow lightning scheme
2020-04-28 00:00:19 +02:00
Robert Habermann 0847a1c933 add lightning scheme check 2020-04-27 23:13:42 +02:00
ArcandGitHub f731bd766e Update and rename config.json to config.json.example 2020-04-27 22:01:21 +01:00
ArcandGitHub 59819e2875 Update _api_docs.html 2020-04-27 21:08:31 +01:00
ArcandGitHub e7184346ab Update _api_docs.html 2020-04-27 21:08:17 +01:00
Eneko Illarramendi 178fe65fb2 test: configure Travis for CI 2020-04-27 19:39:00 +02:00
ArcandGitHub 1c8beba80b Added warning to use https or TOR for lnurl 2020-04-27 17:43:29 +01:00
ArcandGitHub 1516a14a0d Added warning to use https or TOR for lnurl 2020-04-27 17:43:15 +01:00
142 changed files with 9385 additions and 6094 deletions
+2 -1
View File
@@ -2,13 +2,14 @@ FLASK_APP=lnbits
FLASK_ENV=development
LNBITS_SITE_TITLE=LNbits
LNBITS_ALLOWED_USERS="all"
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_DATA_FOLDER="/your_custom_data_folder"
LNBITS_DISABLED_EXTENSIONS="amilk,events"
LNBITS_FORCE_HTTPS=1
LNBITS_SERVICE_FEE="0.0"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet, CLightningWallet, LnbitsWallet
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC), LndRestWallet, CLightningWallet, LnbitsWallet
LNBITS_BACKEND_WALLET_CLASS=LntxbotWallet
CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
+1 -2
View File
@@ -1,2 +1 @@
lnbits/static/vendor/** linguist-vendored
lnbits/extensions/**/static/vendor/** linguist-vendored
**/static/vendor/** linguist-vendored
+1
View File
@@ -0,0 +1 @@
custom: https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK
+54
View File
@@ -0,0 +1,54 @@
name: "CodeQL"
on:
push:
branches: [master, ]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 12 * * 5'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+25
View File
@@ -0,0 +1,25 @@
name: Run Linters
on: [push, pull_request]
jobs:
mypy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Run MyPy python type checker
uses: jpetrucciani/mypy-check@master
with:
path: 'lnbits'
prettier:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Check JS code formatting convention
uses: creyD/prettier_action@v2.2
with:
dry: True
prettier_options: --write lnbits/static/js/** lnbits/core/static/js/** lnbits/extensions/*/templates/**
-13
View File
@@ -1,13 +0,0 @@
name: mypy type checker
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: jpetrucciani/mypy-check@master
with:
path: 'lnbits'
+38
View File
@@ -0,0 +1,38 @@
name: test suite
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Test with pytest
env:
LNBITS_BACKEND_WALLET_CLASS: LNPayWallet
LNBITS_FORCE_HTTPS: 0
LNPAY_API_ENDPOINT: https://lnpay.co/v1/
LNPAY_API_KEY: sak_gG5pSFZhFgOLHm26a8hcWvXKt98yd
LNPAY_ADMIN_KEY: waka_HqWfOoNE0TPqmQHSYErbF4n9
LNPAY_INVOICE_KEY: waki_ZqFEbhrTyopuPlOZButZUw
LNPAY_READ_KEY: wakr_6IyTaNrvSeu3jbojSWt4ou6h
run: |
pip install pytest pytest-cov
pytest --cov=lnbits --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
file: ./coverage.xml
+2
View File
@@ -27,3 +27,5 @@ venv
.pyre*
__bundle__
node_modules
+12
View File
@@ -0,0 +1,12 @@
{
"semi": false,
"arrowParens": "avoid",
"insertPragma": false,
"printWidth": 80,
"proseWrap": "preserve",
"singleQuote": true,
"trailingComma": "none",
"useTabs": false,
"jsxBracketSameLine": false,
"bracketSpacing": false
}
-29
View File
@@ -1,29 +0,0 @@
For developers
==============
LNbits uses [Flask](http://flask.pocoo.org/).
Feel free to contribute to the project.
Application dependencies
------------------------
The application uses [Pipenv][pipenv] to manage Python packages.
While in development, you will need to install all dependencies:
$ pipenv shell
$ pipenv install --dev
Running the server
------------------
$ flask run
There is an environment variable called `FLASK_ENV` that has to be set to `development`
if you want to run Flask in debug mode with autoreload
Style guide
-----------
Tab size is 4 spaces. Maximum line length is 120. You should run `black` before commiting any change.
$ black lnbits
[pipenv]: https://docs.pipenv.org/#install-pipenv-today
+2
View File
@@ -0,0 +1,2 @@
prettier:
./node_modules/.bin/prettier --write lnbits/static/js/** lnbits/core/static/js/** lnbits/extensions/*/templates/**
+2 -4
View File
@@ -9,15 +9,14 @@ python_version = "3.7"
[packages]
bitstring = "*"
cerberus = "*"
lnd-grpc = "*"
lnurl = "*"
flask = "*"
flask-assets = "*"
flask-compress = "*"
flask-cors = "*"
flask-talisman = "*"
gevent = "*"
gevent = "==20.5.0"
gunicorn = "*"
pylightning = "*"
pyscss = "*"
requests = "*"
shortuuid = "*"
@@ -28,4 +27,3 @@ flake8 = "*"
flake8-mypy = "*"
pytest = "*"
pytest-cov = "*"
pytest-sugar = "*"
Generated
+271 -347
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "1c40bedbf1f2a9e9bccd443b57bd6dda496361df8712ddda5efcec0d00cab9f1"
"sha256": "76587c919e4a0d49f3a4ff297353ec8b8c92521effdb5eaf985cb32213ebc5c5"
},
"pipfile-spec": 6,
"requires": {
@@ -21,16 +21,53 @@
"sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899",
"sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"
],
"markers": "python_version >= '3.5'",
"version": "==1.2.0"
},
"bitstring": {
"hashes": [
"sha256:7b60b0c300d0d3d0a24ec84abfda4b0eaed3dc56dc90f6cbfe497166c9ad8443",
"sha256:c97a8e2a136e99b523b27da420736ae5cb68f83519d633794a6a11192f69f8bf",
"sha256:e392819965e7e0246e3cf6a51d5a54e731890ae03ebbfa3cd0e4f74909072096"
"sha256:fdf3eb72b229d2864fb507f8f42b1b2c57af7ce5fec035972f9566de440a864a"
],
"index": "pypi",
"version": "==3.1.6"
"version": "==3.1.7"
},
"brotli": {
"hashes": [
"sha256:0538dc1744fd17c314d2adc409ea7d1b779783b89fd95bcfb0c2acc93a6ea5a7",
"sha256:0970a47f471782912d7705160b2b0a9306e68e6fadf9cffcaeb42d8f0951e26c",
"sha256:113f51658e6fe548dce4b3749f6ef6c24de4184ba9c10a909cbee4261c2a5da0",
"sha256:1e1aa9c4d1558889f42749c8baf846007953bfd32c8209230cf1cd1f5ef33495",
"sha256:2f2f4f78f29ac4a45d15b3d9fc3fd9705e0ad313a44b129f6e1d0c6916bad0e2",
"sha256:3269f6de1dd150fd0cce1c158b61ff5ac06d627fd3ae9c6ea03aed26fbbff7ea",
"sha256:3f4a1f6240916c7984c7f2542786710f622992508dafee0b1714e6d340fb9ffd",
"sha256:50dd9ad2a2bb12da4e9002a438672d182f98e546e99952de80280a1e1729664f",
"sha256:5519a4b01b1a4f965083cbfa2ef2b9774c5a5f352341c47b50776ad109423d72",
"sha256:5eb27722d320370315971c427eb8aa7cc0791f2a458840d357ac653bd0ad3a14",
"sha256:5f06b4d5b6f58e5b5c220c2f23cad034dc5efa51b01fde2351ced1605bd980e2",
"sha256:71ceee286ea7ec613f1c36f1c6181864a6ca24ebb55e371276f33d6af8742834",
"sha256:72848d25a5f9e736db4af4512e0c3feecc094d57d241f8f1ae959115a2c39756",
"sha256:743001bca75f4a6b4454be3510feca46f9d61a0c782a9bc2bc684bdb245e279e",
"sha256:7ac98c71a15648fd11bc1f32608b6110e396121280790082e32b9a3109048bc6",
"sha256:9d1c2dd27a1083fefd05b1b2f8df4a6bc2aaa6c21dd82cd41c8ae5e7c23a87f8",
"sha256:a13ce9b419fe9f277c63f700efb0e444331509d1881b5610d2ba7e9080606967",
"sha256:a19ef0952b9d2803df88dff07f45a6c92d5676afb9b8d69cf32232d684036d11",
"sha256:ad766ca8b8c1419b71a22756b45264f45725c86133dc80a7cbe30b6b78c75620",
"sha256:ad7963f261988ee0883816b6b9f206f11461c9b3cb5cfbca0c9ab5adc406d395",
"sha256:af0451e23016631a2f52925a10d738ac4a0f794ac315c30380b22efc0c90cbc6",
"sha256:c16201060c5a3f8742e3deae759014251ac92f382f82bc2a41dc079ff18c3f24",
"sha256:c43b202f65891861a9a336984a103de25de235f756de69e32db893156f767013",
"sha256:c675c6cce4295cb1a692f3de7416aacace7314e064b94bc86e93aceefce7fd3e",
"sha256:d17cec0b992b1434f5f9df9986563605a4d1b1acd5574c87fc2ac014bcbd3316",
"sha256:dc91f6129953861a73d9a65c52a8dd682b561a9ebaf65283541645cab6489917",
"sha256:e2f4cbd1760d2bf2f30e396c2301999aab0191aec031a6a8a04950b2f575a536",
"sha256:f192e6d3556714105c10486bbd6d045e38a0c04d9da3cef21e0a8dfd8e162df4",
"sha256:f775b07026af2b1b0b5a8b05e41571cdcf3a315a67df265d60af301656a5425b",
"sha256:f969ec7f56ba9636679e69ca07fba548312ccaca37412ee823c7f413541ad7e0",
"sha256:f9dc52cd70907aafb99a773b66b156f2f995c7a0d284397c487c8b71ddbef2f9",
"sha256:f9ee88bb52352588ceb811d045b5c9bb1dc38927bc150fd156244f60ff3f59f1",
"sha256:fc7212e36ebeb81aebf7949c92897b622490d7c0e333a479c0395591e7994600"
],
"version": "==1.0.7"
},
"cerberus": {
"hashes": [
@@ -41,10 +78,10 @@
},
"certifi": {
"hashes": [
"sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304",
"sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
],
"version": "==2020.4.5.1"
"version": "==2020.6.20"
},
"chardet": {
"hashes": [
@@ -55,10 +92,11 @@
},
"click": {
"hashes": [
"sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
"sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"version": "==7.1.1"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"flask": {
"hashes": [
@@ -78,10 +116,18 @@
},
"flask-compress": {
"hashes": [
"sha256:468693f4ddd11ac6a41bca4eb5f94b071b763256d54136f77957cfee635badb3"
"sha256:f367b2b46003dd62be34f7fb1379938032656dca56377a9bc90e7188e4289a7c"
],
"index": "pypi",
"version": "==1.4.0"
"version": "==1.5.0"
},
"flask-cors": {
"hashes": [
"sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16",
"sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"
],
"index": "pypi",
"version": "==3.0.8"
},
"flask-talisman": {
"hashes": [
@@ -93,139 +139,54 @@
},
"gevent": {
"hashes": [
"sha256:0b84a8d6f088b29a74402728681c9f11864b95e49f5587a666e6fbf5c683e597",
"sha256:1ef086264e846371beb5742ebaeb148dc96adf72da2ff350ae5603421cdc2ad9",
"sha256:2070c65896f89a85b39f49427d6132f7abd047129fc4da88b3670f0ba13b0cf7",
"sha256:2fbe0bc43d8c5540153f06eece6235dda14e5f99bdd9183838396313100815d7",
"sha256:32813de352918fb652a3db805fd6e08e0a1666a1a9304eef95938c9c426f9573",
"sha256:38c45d8a3b647f56f8a68769a8ac4953be84a84735c7c7a4d7ca62022bd54036",
"sha256:3b4c4d99f87c0d04b825879c5a91fbfa2b66da7c25b8689e9bdd9f4741d5f80d",
"sha256:42cae3be36b7458f411bd589c66aaba27e4e611ec3d3621e37fd732fe383f9b6",
"sha256:4572dc7907a0ac3c39b9f0898dbdf390ae3250baaae5f7395661fb844e2e23be",
"sha256:6088bedd8b6bcdb815be322304a5d1c028ffa837d84e93b349928dadac62f354",
"sha256:8a9aba59a3268f20c7b584119215bdc589cb81500d93dad4dab428eb02f72944",
"sha256:8cca7ffd58559f8d51e5605ad73afcc6f348f9747d2fa539b336e70851b69b79",
"sha256:956e82a5d0e90f8d71efe4cecccde602cfb657cd866c58bb953c9c30ca1b3d77",
"sha256:b0aea12de542f8fcd6882087bdd5b4d7dc8bb316d28181f6b012dd0b91583285",
"sha256:b46399f6c9eccc2e6de1dc1057d362be840443e5439b06cce8b01d114ba1a7ec",
"sha256:c0b38a654c8fde5b9d9bd27ea3261aeefe36bc9244b170b6d3b11d72a2163bdb",
"sha256:c516cc5d70c3faf07f271d50930d144339c69fb80f3cac9b687aa964e518535e",
"sha256:c7a62d51c6dca84f91a91b940037523c926a516f0568f47dc1386bd1682cf4e9",
"sha256:cea28f958bc4206ae092043e0775cd7a2bb2536bcbece292732c6484c1076c01",
"sha256:d56f36eb98532d2bccc51cb0964c31e9fbd9b2282074c297dc9b006b047e2966",
"sha256:de6c0cbcb890d0a79323961d3b593a0f2f54dcb9fe38ee5167f2d514e69e3c8c",
"sha256:e0990009e7c1624f9a0f3335df1ab8d45678241c852659ac645b70ed8229097c",
"sha256:e7d23d5f32c9db6ae49c4b58585618dcafd6ad0babae251c9c8297afebc4744b",
"sha256:ee39caf14d66e619709cdfe3962bc68a234518e43ea8c811c0d67a864bc7c196"
"sha256:00b03601b8dd1ee2aa07811cb60a4befe36173b15d91c6e207e37f8d77dd6fac",
"sha256:0acc15ba2ac2a555529ad82d5a28fc85dbb6b2ff947657d67bebfd352e2b5c14",
"sha256:15eae3cd450dac7dae7f4ac59e01db1378965c9ef565c39c5ae78c5a888f9ac9",
"sha256:1dc7f1f6bc1f67d625e4272b01e717eba0b4fa024d2ff7934c8d320674d6f7fa",
"sha256:1dd95433be45e1115053878366e3f5332ae99c39cb345be23851327c062b9f4a",
"sha256:28b7d83b4327ceb79668eca2049bf4b9ce66d5ace18a88335e3035b573f889fd",
"sha256:31dc5d4ab8172cc00c4ff17cb18edee633babd961f64bf54214244d769bc3a74",
"sha256:38db524ea88d81d596b2cbb6948fced26654a15fec40ea4529224e239a6f45e8",
"sha256:3ff477b6d275396123faf8ce2d5b82f96d85ba264e0b9d4b56a2bac49d1b9adc",
"sha256:4d2729dd4bf9c4d0f29482f53cdf9fc90a498aebb5cd7ae8b45d35657437d2ac",
"sha256:52e5cd607749ed3b8aa0272cacf2c11deec61fca4c3bec57a9fea8c49316627d",
"sha256:5c604179cebcc57f10505d8db177b92a715907815a464b066e7eba322d1c33ac",
"sha256:88c76df4967c5229f853aa67ad1b394d9e4f985b0359c9bc9879416bba3e7c68",
"sha256:929c33df8e9bcbe31906024fcd21580bd018196dbd3249eb5b2f19d63e11092d",
"sha256:92edc18a357473e01a4e4a82c073ed3c99ceca6e3ce93c23668dd4a2401f07dc",
"sha256:937d36730f2b0dee3387712074b1f15b802e2e074a3d7c6dcaf70521236d607c",
"sha256:9b4e940fc6071afebb86ba5f48dbb5f1fc3cb96ebeb8cf145eb5b499e9c6ee33",
"sha256:a7805934e8ce81610b61f806572c3d504cedd698cc8c9460d78d2893ba598c4a",
"sha256:d07a2afe4215731eb57d5b257a2e7e7e170d8a7ae1f02f6d0682cd3403debea9",
"sha256:e01d5373528e4ebdde66dc47a608d225fa3c4408ccd828d26c49b7ff75d82bd9",
"sha256:efd9546468502a30ddd4699c3124ccb9d3099130f9b5ae1e2a54ad5b46e86120",
"sha256:fcb64f3a28420d1b872b7ef41b12e8a1a4dcadfc8eff3c09993ab0cdf52584a1"
],
"index": "pypi",
"version": "==20.4.0"
},
"googleapis-common-protos": {
"hashes": [
"sha256:013c91704279119150e44ef770086fdbba158c1f978a6402167d47d5409e226e"
],
"version": "==1.51.0"
"version": "==20.5.0"
},
"greenlet": {
"hashes": [
"sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0",
"sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28",
"sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8",
"sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304",
"sha256:51155342eb4d6058a0ffcd98a798fe6ba21195517da97e15fca3db12ab201e6e",
"sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0",
"sha256:7457d685158522df483196b16ec648b28f8e847861adb01a55d41134e7734122",
"sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214",
"sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043",
"sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6",
"sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625",
"sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc",
"sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638",
"sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163",
"sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4",
"sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490",
"sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248",
"sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939",
"sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87",
"sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720",
"sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656",
"sha256:e538b8dae561080b542b0f5af64d47ef859f22517f7eca617bb314e0e03fd7ef"
"sha256:1000038ba0ea9032948e2156a9c15f5686f36945e8f9906e6b8db49f358e7b52",
"sha256:133ba06bad4e5f2f8bf6a0ac434e0fd686df749a86b3478903b92ec3a9c0c90b",
"sha256:1429dc183b36ec972055e13250d96e174491559433eb3061691b446899b87384",
"sha256:1b805231bfb7b2900a16638c3c8b45c694334c811f84463e52451e00c9412691",
"sha256:3a35e33902b2e6079949feed7a2dafa5ac6f019da97bd255842bb22de3c11bf5",
"sha256:5ea034d040e6ab1d2ae04ab05a3f37dbd719c4dee3804b13903d4cc794b1336e",
"sha256:682328aa576ec393c1872615bcb877cf32d800d4a2f150e1a5dc7e56644010b1",
"sha256:6e06eac722676797e8fce4adb8ad3dc57a1bb3adfb0dd3fdf8306c055a38456c",
"sha256:7eed31f4efc8356e200568ba05ad645525f1fbd8674f1e5be61a493e715e3873",
"sha256:80cb0380838bf4e48da6adedb0c7cd060c187bb4a75f67a5aa9ec33689b84872",
"sha256:b0b2a984bbfc543d144d88caad6cc7ff4a71be77102014bd617bd88cfb038727",
"sha256:c196a5394c56352e21cb7224739c6dd0075b69dd56f758505951d1d8d68cf8a9",
"sha256:d83c1d38658b0f81c282b41238092ed89d8f93c6e342224ab73fb39e16848721",
"sha256:df7de669cbf21de4b04a3ffc9920bc8426cab4c61365fa84d79bf97401a8bef7",
"sha256:e5db19d4a7d41bbeb3dd89b49fc1bc7e6e515b51bbf32589c618655a0ebe0bf0",
"sha256:e695ac8c3efe124d998230b219eb51afb6ef10524a50b3c45109c4b77a8a3a92",
"sha256:eac2a3f659d5f41d6bbfb6a97733bc7800ea5e906dc873732e00cebb98cec9e4"
],
"markers": "platform_python_implementation == 'CPython'",
"version": "==0.4.15"
},
"grpcio": {
"hashes": [
"sha256:085bbf7fd0070b8d65e84aa32979f17cfe624d27b5ce23955ef770c19d2d9623",
"sha256:0ae207a47ec0ad66eb1f53a27d566674d13a236c62ced409891335318ea9b8c5",
"sha256:0c130204ff5de0b9f041bf3126db0d29369d69883592e4b0d3c19868ba0ced7e",
"sha256:0ef6b380a588c2c6b29c6cfa0ba7f5d367beb33d5504bcc68658fa241ad498d2",
"sha256:16e1edb367763ea08d0994d4635ec05f4f8db9db59c39304b061097e3b93df43",
"sha256:16f5523dacae5aaeda4cf900da7e980747f663298c38c18eb4e5317704aa007a",
"sha256:181b5078cf568f37915b8a118afcef5fc9f3128c59c38998ed93e7dd793e3928",
"sha256:245564713cb4ac7bccb0f11be63781beb62299a44d8ab69031c859dbd9461728",
"sha256:271abbe28eb99fa5c70b3f272c0c66b67dab7bb11e1d29d8e616b4e0e099d29a",
"sha256:2e1b01cba26988c811c7fb91a0bca19c9afb776cc3d228993f08d324bdd0510a",
"sha256:3366bd6412c1e73acb1ee27d7f0c7d7dbee118ad8d98c957c8173691b2effeec",
"sha256:3893b39a0a17d857dc3a42fdb02a26aa53a59bfce49987187bcc0261647f1f55",
"sha256:3c7864d5ae63b787001b01b376f6315aef1a015aa9c809535235ed0ead907919",
"sha256:42c6716adf3ec1f608b2b56e885f26dd86e80d2fc1617f51fc92d1b0b649e28e",
"sha256:4bef0756b9e0df78e8d67a5b1e0e89b7daf41525d575f74e1f14a993c55b680d",
"sha256:4fe081862e58b8fbef0e479aefc9a64f8f17f53074df1085d8c1fe825a6e5df4",
"sha256:505a8d1b4ac571a51f10c4c995d5d4714f03c886604dc3c097ef5fd57bcfcf0b",
"sha256:5c2e81b6ab9768c43f2ca1c9a4c925823aad79ae95efb351007df4b92ebce592",
"sha256:70ff2df0c1795c5cf585a72d95bb458838b40bad5653c314b9067ba819e918f9",
"sha256:97b5612fc5d4bbf0490a2d80bed5eab5b59112ef1640440c1a9ac824bafa6968",
"sha256:a35f8f4a0334ed8b05db90383aecef8e49923ab430689a4360a74052f3a89cf4",
"sha256:aafe85a8210dfa1da3c46831b7f00c3735240b7b028eeba339eaea6ffdb593fb",
"sha256:c2e53eb253840f05278a8410628419ba7060815f86d48c9d83b6047de21c9956",
"sha256:c3645887db3309fc87c3db740b977d403fb265ebab292f1f6a926c4661231fd5",
"sha256:c6565cc92853af13237b2233f331efdad07339d27fe1f5f74256bfde7dc2f587",
"sha256:cbc322c5d5615e67c2a15be631f64e6c2bab8c12505bc7c150948abdaa0bdbac",
"sha256:df749ee982ec35ab76d37a1e637b10a92b4573e2b4e1f86a5fa8a1273c40a850",
"sha256:e9439d7b801c86df13c6cbb4c5a7e181c058f3c119d5e119a94a5f3090a8f060",
"sha256:f493ac4754717f25ace3614a51dd408a32b8bff3c9c0c85e9356e7e0a120a8c8",
"sha256:f80d10bdf1a306f7063046321fd4efc7732a606acdd4e6259b8a37349079b704",
"sha256:f83b0c91796eb42865451a20e82246011078ba067ea0744f7301e12a94ae2e1b"
],
"version": "==1.28.1"
},
"grpcio-tools": {
"hashes": [
"sha256:095911bd21dd2dc029b8862b44e65b41341135fd16da60ad9d0df4b8e9a0618e",
"sha256:17a6b63ee1650abbbc9306f3144d1d3bd3d088a270ca6569afcb68a8cc4a4a38",
"sha256:27fcab84ea948d965f82c7994903e8e36a20ce4ad7ef55a22c8d7c639e6c8808",
"sha256:2ffa5f6fbd26ea6c7626bf89d6ef4d64d111000a8bd4e9df69b070093de0f85d",
"sha256:491dcd98d988c0a4ed9bbb1ec6310588fc0e16b334b64119b973363a1cf96cc1",
"sha256:493b5a275ff2692942951926a259e396ff13acec72e6d93498638006b1a1bd71",
"sha256:4c980208c1620e52e86c4037058097673ee8e3bbbec34582f2538e614d0c41f4",
"sha256:55f3a86c3a808b8a5c2dc8112c45329dd912e43a6fb474da94a7e04d8bb36e1a",
"sha256:69a60e1c020547668f313b7a81d653f8c820066ab0f6ed979af760905b61d9e1",
"sha256:6f7806fe53e5979fa10ec49cbd5236cf0e57c8817ce54350bccc8ebceaeb54fb",
"sha256:8f752fa03eb2d851aac5838aa46a5b831660e9800634fad8ccd46539e9cb4663",
"sha256:9210b3575fee355a4056cff8ff7212b823371e9fa9ad1467e3c0c5cb006d0d7d",
"sha256:9b900cf90b293acd48bf258d5e416eae9540b5aa0c75cc2fe68a7e2c5f4834ae",
"sha256:addc40a47f496cafa108ba1a31838a9832223e0dcafaf424d38e6b8c79fe26ac",
"sha256:adf089aaf6e21358b12e39d9fa7c28611340d8399a918c0b72ff122ce9b7e0af",
"sha256:b22172e3ad5e849815f3e35a002fb67c52e83cd3e4299cf45760de4674c7cd04",
"sha256:b5cf457dcd6499f8c2f2bdcc486d681ca3ee673064484a31c507df6139bf00fe",
"sha256:b5ee6f089bb040d175e658911760584c7a6cb70fbdcc78bdbe6349475933483b",
"sha256:bb9b1a05917011e98e5331d129a40a41f49c5652e1b86d5fea9b3f6c98454928",
"sha256:c527db050fa619a2bd293a73db0e0af42f0bd5a4f41c043fc5da543d81f5fed6",
"sha256:c9efe6420d2ffaf9aa32d292817ddfc939b9adf28a51cef30984bab02ed35a58",
"sha256:cb7b54a96f4d9aae5d9c6b644a69ce48c12cfa3558c86ce0a62d9c8a08a83f2e",
"sha256:d661e5c4f82f9a223b9d1a6c9ec68faea060324e0d25684a6f7fb0b9622ab561",
"sha256:d6ee6f338e0099976161e78ecf91d2ab9c04be75a944ef29facc93a4aad28e1d",
"sha256:d89dec894495c51ee0a8dd0c896e4320e95bab179c3faa6a439ef5719486c35e",
"sha256:dc7ea3739aa6f5abe6e9a27f403b30c7996978176e5884c41ff3d8a153e7c35a",
"sha256:de10f8b28ef3e8a7f336e02b7c2912713bd41e0931c4fad3a6993d25c8511159",
"sha256:e05f7bfb63b844b7f9d8b50019b02168fd8982278dc0443986f3504b3cb87d2a",
"sha256:e942dd349d5be0d5ebbe96ed9cbcfb5fdb0bc7b57357754c42db8083a0ec80aa",
"sha256:edc01f022a878722ae69bff3faa8c6ec7e967d57b7e8a5f4b2f6100ad903a978",
"sha256:f61d219d06057e015d662c0b40438f119f58e514a5bebf425058a34d05ff21db"
],
"version": "==1.28.1"
"version": "==0.4.16"
},
"gunicorn": {
"hashes": [
@@ -237,16 +198,18 @@
},
"idna": {
"hashes": [
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.9"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.10"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.0"
},
"jinja2": {
@@ -254,23 +217,15 @@
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.11.2"
},
"lnd-grpc": {
"hashes": [
"sha256:43114aead2dc2d008751231784fc94027d60569a5e4dc86ed68032a2203fe781",
"sha256:c28208d912b28f5b0b11fa87559c6e564731549ecbc52eafe7d5dd2b2f469cf8"
],
"index": "pypi",
"version": "==0.4.0"
},
"lnurl": {
"hashes": [
"sha256:633ddedddcff4bd8b7790eba1649ec10c91fb28e690e3f6e984cb8591be3b626",
"sha256:749e0d73a8e5d41b03c734df4f22c0e022f1da4f64e600a898b56e1bb75f5dda"
"sha256:398fcb58b8112575bb24a445c61dda0e180df232717ccdbcb6a6932221cc6d16"
],
"index": "pypi",
"version": "==0.3.3"
"version": "==0.3.4"
},
"markupsafe": {
"hashes": [
@@ -308,60 +263,31 @@
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1"
},
"protobuf": {
"hashes": [
"sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab",
"sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f",
"sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a",
"sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0",
"sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4",
"sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2",
"sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee",
"sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07",
"sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151",
"sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a",
"sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f",
"sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7",
"sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956",
"sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306",
"sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961",
"sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481",
"sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a",
"sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80"
],
"version": "==3.11.3"
},
"pydantic": {
"hashes": [
"sha256:0b7aadfa1de28057656064e04d9f018d1b186fe2a8e953a2fb41545873b7cf95",
"sha256:0f61e67291b99a927816558a218a4e794db72a33621c836e63d12613a2202cd4",
"sha256:20946280c750753b3e3177c748825ef189d7ab86c514f6a0b118621110d5f0d3",
"sha256:22139ee446992c222977ac0a9269c4da2e9ecc1834f84804ebde008a4649b929",
"sha256:3c0f39e884d7a3572d5cc8322b0fe9bf66114283e22e05a5c4b8961c19588945",
"sha256:446ce773a552a2cb90065d4aa645e16fa7494369b5f0d199e4d41a992a98204d",
"sha256:475e6606873e40717cc3b0eebc7d1101cbfc774e01dadeeea24c121eb5826b86",
"sha256:66124752662de0479a9d0c17bdebdc8a889bccad8846626fb66d8669e8eafb63",
"sha256:896637b7d8e4cdc0bcee1704fcadacdd167c35ac29f02a4395fce7a033925f26",
"sha256:9af44d06db33896a2176603c9cb876df3a60297a292a24d3018956a910cc1402",
"sha256:9e46fac8a4674db0777fd0133aa56817e1481beee50971bab39dded7639f9b2b",
"sha256:ae206e103e976c40ec294cd6c8fcbfbdaced3ab9b736bc53d03fa11b5aaa1628",
"sha256:b11d0bd7ecf41098894e8777ee623c29554dbaa37e862c51bcc5a2b950d1bf77",
"sha256:d73070028f7b046a5b2e611a9799c238d7bd245f8fe30f4ad7ff29ddb63aac40",
"sha256:ddedcdf9d5c24939578449a8e099ceeec3b3d76243fc143aff63ebf6d5aade10",
"sha256:e08e21f4d5395ac17cde19de26be63fb16fb870f0cfde1481ddc22d5e2353548",
"sha256:e6239199b363bc53262bcb57f1441206d4b2d46b392eccba2213d8358d6e284a"
"sha256:1783c1d927f9e1366e0e0609ae324039b2479a1a282a98ed6a6836c9ed02002c",
"sha256:2dc946b07cf24bee4737ced0ae77e2ea6bc97489ba5a035b603bd1b40ad81f7e",
"sha256:2de562a456c4ecdc80cf1a8c3e70c666625f7d02d89a6174ecf63754c734592e",
"sha256:36dbf6f1be212ab37b5fda07667461a9219c956181aa5570a00edfb0acdfe4a1",
"sha256:3fa799f3cfff3e5f536cbd389368fc96a44bb30308f258c94ee76b73bd60531d",
"sha256:40d765fa2d31d5be8e29c1794657ad46f5ee583a565c83cea56630d3ae5878b9",
"sha256:418b84654b60e44c0cdd5384294b0e4bc1ebf42d6e873819424f3b78b8690614",
"sha256:4900b8820b687c9a3ed753684337979574df20e6ebe4227381d04b3c3c628f99",
"sha256:530d7222a2786a97bc59ee0e0ebbe23728f82974b1f1ad9a11cd966143410633",
"sha256:54122a8ed6b75fe1dd80797f8251ad2063ea348a03b77218d73ea9fe19bd4e73",
"sha256:6c3f162ba175678218629f446a947e3356415b6b09122dcb364e58c442c645a7",
"sha256:b49c86aecde15cde33835d5d6360e55f5e0067bb7143a8303bf03b872935c75b",
"sha256:b5b3489cb303d0f41ad4a7390cf606a5f2c7a94dcba20c051cd1c653694cb14d",
"sha256:cf3933c98cb5e808b62fae509f74f209730b180b1e3c3954ee3f7949e083a7df",
"sha256:eb75dc1809875d5738df14b6566ccf9fd9c0bcde4f36b72870f318f16b9f5c20",
"sha256:f769141ab0abfadf3305d4fcf36660e5cf568a666dd3efab7c3d4782f70946b1",
"sha256:f8af9b840a9074e08c0e6dc93101de84ba95df89b267bf7151d74c553d66833b"
],
"version": "==1.5"
},
"pylightning": {
"hashes": [
"sha256:5b7df2c6f0faf17450ea3024f29f901a114d4c68da116a1b130958df510a0557",
"sha256:6a07b62b27e01baaf51165e2dfb8b7d1d47f61aef8304a9da3ca5081608a32c0"
],
"index": "pypi",
"version": "==0.0.7.3"
"markers": "python_version >= '3.6'",
"version": "==1.6.1"
},
"pyscss": {
"hashes": [
@@ -372,11 +298,11 @@
},
"requests": {
"hashes": [
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
],
"index": "pypi",
"version": "==2.23.0"
"version": "==2.24.0"
},
"shortuuid": {
"hashes": [
@@ -388,10 +314,11 @@
},
"six": {
"hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"version": "==1.14.0"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
"typing-extensions": {
"hashes": [
@@ -404,10 +331,11 @@
},
"urllib3": {
"hashes": [
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
],
"version": "==1.25.9"
"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": "==1.25.10"
},
"webassets": {
"hashes": [
@@ -421,22 +349,24 @@
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.0.1"
}
},
"develop": {
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
],
"version": "==1.4.3"
"version": "==1.4.4"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==19.3.0"
},
"black": {
@@ -449,61 +379,59 @@
},
"click": {
"hashes": [
"sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
"sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"version": "==7.1.1"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"coverage": {
"hashes": [
"sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
"sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
"sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
"sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
"sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
"sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
"sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
"sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
"sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
"sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
"sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
"sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
"sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
"sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
"sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
"sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
"sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
"sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
"sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
"sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
"sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
"sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
"sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
"sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
"sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
"sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
"sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
"sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
"sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
"sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
"sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
"sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb",
"sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3",
"sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716",
"sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034",
"sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3",
"sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8",
"sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0",
"sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f",
"sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4",
"sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962",
"sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d",
"sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b",
"sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4",
"sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3",
"sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258",
"sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59",
"sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01",
"sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd",
"sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b",
"sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d",
"sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89",
"sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd",
"sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b",
"sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d",
"sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46",
"sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546",
"sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082",
"sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b",
"sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4",
"sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8",
"sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811",
"sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd",
"sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651",
"sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"
],
"version": "==5.1"
},
"entrypoints": {
"hashes": [
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
],
"version": "==0.3"
"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.2.1"
},
"flake8": {
"hashes": [
"sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
"sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"
"sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c",
"sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"
],
"index": "pypi",
"version": "==3.7.9"
"version": "==3.8.3"
},
"flake8-mypy": {
"hashes": [
@@ -515,11 +443,18 @@
},
"importlib-metadata": {
"hashes": [
"sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f",
"sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"
"sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83",
"sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
],
"markers": "python_version < '3.8'",
"version": "==1.6.0"
"version": "==1.7.0"
},
"iniconfig": {
"hashes": [
"sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
"sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"
],
"version": "==1.0.1"
},
"mccabe": {
"hashes": [
@@ -530,29 +465,31 @@
},
"more-itertools": {
"hashes": [
"sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c",
"sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"
"sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5",
"sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"
],
"version": "==8.2.0"
"markers": "python_version >= '3.5'",
"version": "==8.4.0"
},
"mypy": {
"hashes": [
"sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2",
"sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1",
"sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164",
"sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761",
"sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce",
"sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27",
"sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754",
"sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae",
"sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9",
"sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600",
"sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65",
"sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8",
"sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913",
"sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3"
"sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c",
"sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86",
"sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b",
"sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd",
"sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc",
"sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea",
"sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e",
"sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308",
"sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406",
"sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d",
"sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707",
"sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d",
"sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c",
"sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a"
],
"version": "==0.770"
"markers": "python_version >= '3.5'",
"version": "==0.782"
},
"mypy-extensions": {
"hashes": [
@@ -563,10 +500,11 @@
},
"packaging": {
"hashes": [
"sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3",
"sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
],
"version": "==20.3"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.4"
},
"pathspec": {
"hashes": [
@@ -580,105 +518,97 @@
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.13.1"
},
"py": {
"hashes": [
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
],
"version": "==1.8.1"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.9.0"
},
"pycodestyle": {
"hashes": [
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
],
"version": "==2.5.0"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.6.0"
},
"pyflakes": {
"hashes": [
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
],
"version": "==2.1.1"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.2.0"
},
"pyparsing": {
"hashes": [
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7"
},
"pytest": {
"hashes": [
"sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172",
"sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"
"sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4",
"sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad"
],
"index": "pypi",
"version": "==5.4.1"
"version": "==6.0.1"
},
"pytest-cov": {
"hashes": [
"sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b",
"sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"
"sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191",
"sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"
],
"index": "pypi",
"version": "==2.8.1"
},
"pytest-sugar": {
"hashes": [
"sha256:26cf8289fe10880cbbc130bd77398c4e6a8b936d8393b116a5c16121d95ab283",
"sha256:fcd87a74b2bce5386d244b49ad60549bfbc4602527797fac167da147983f58ab"
],
"index": "pypi",
"version": "==0.9.2"
"version": "==2.10.1"
},
"regex": {
"hashes": [
"sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b",
"sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8",
"sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3",
"sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e",
"sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683",
"sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1",
"sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142",
"sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3",
"sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468",
"sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e",
"sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3",
"sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a",
"sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f",
"sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6",
"sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156",
"sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b",
"sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db",
"sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd",
"sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a",
"sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948",
"sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"
"sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204",
"sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162",
"sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f",
"sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb",
"sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6",
"sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7",
"sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88",
"sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99",
"sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644",
"sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a",
"sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840",
"sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067",
"sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd",
"sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4",
"sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e",
"sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89",
"sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e",
"sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc",
"sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf",
"sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341",
"sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"
],
"version": "==2020.4.4"
"version": "==2020.7.14"
},
"six": {
"hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"version": "==1.14.0"
},
"termcolor": {
"hashes": [
"sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"
],
"version": "==1.1.0"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
],
"version": "==0.10.0"
"version": "==0.10.1"
},
"typed-ast": {
"hashes": [
@@ -715,18 +645,12 @@
"markers": "python_version < '3.8'",
"version": "==3.7.4.2"
},
"wcwidth": {
"hashes": [
"sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1",
"sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"
],
"version": "==0.1.9"
},
"zipp": {
"hashes": [
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
],
"markers": "python_version >= '3.6'",
"version": "==3.1.0"
}
}
+37 -40
View File
@@ -1,75 +1,72 @@
LNbits
======
[![github-actions-badge]][github-actions]
[![codecov-badge]][codecov]
[![license-badge]](LICENSE)
![Lightning network wallet](https://i.imgur.com/EHvK6Lq.png)
# LNbits v0.1 BETA, free and open-source lightning-network wallet/accounts system
https://lnbits.com, or run your own LNbits server!
LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
# LNbits v0.1 BETA, free and open-source lightning-network wallet/accounts system
Use [lnbits.com](https://lnbits.com), or run your own LNbits server!
LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet
* Extendable platform for exploring lightning-network functionality via LNbits extension framework
* Extendable platform for exploring lightning-network functionality via LNbits extension framework
* Part of a development stack via LNbits API
* Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations
* Instant wallet for LN demonstrations
The wallet can run on top of any lightning-network funding source, currently there is support for LND, CLightning, Lntxbot, LNpay, OpenNode, with more being added regularily.
The wallet can run on top of any lightning-network funding source, currently there is support for LND, CLightning, Lntxbot, LNpay, OpenNode, with more being added regularily.
See [lnbits.org](https://lnbits.org) for more detailed documentation.
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
LNbits is inspired by all the great work of [opennode.com](https://www.opennode.com/), and in particular [lnpay.co](https://lnpay.co/). Both work as excellent funding sources for LNbits!
## LNbits as an account system
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv + more to come..
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv + more to come..
![Lightning network wallet](https://i.imgur.com/w8jdGpF.png)
Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
(LNbits M5StackSats available here https://github.com/arcbtc/M5StackSats)
(LNbits M5StackSats available here https://github.com/arcbtc/M5StackSats)
![lnurl ATM](https://i.imgur.com/WfCg8wY.png)
## LNbits as an LNURL-withdraw fallback
LNURL has a fallback scheme, so if scanned by a regular QR code reader it can default to a URL. LNbits exploits this to generate an instant wallet using the LNURL-withdraw.
LNURL has a fallback scheme, so if scanned by a regular QR code reader it can default to a URL. LNbits exploits this to generate an instant wallet using the [LNURL-withdraw](https://github.com/btcontract/lnurl-rfc/blob/master/lnurl-withdraw.md).
![lnurl fallback](https://i.imgur.com/CPBKHIv.png)
https://github.com/btcontract/lnurl-rfc/blob/master/spec.md
Adding **/lnurlwallet?lightning="LNURL-withdraw"** will trigger a withdraw that builds an LNbits wallet.
Using **lnbits.com/?lightning="LNURL-withdraw"** will trigger a withdraw that builds an LNbits wallet.
Example use would be an ATM, which utilises LNURL, if the user scans the QR with a regular QR code scanner app, they will stilll be able to access the funds.
![lnurl ATM](https://i.imgur.com/Gi6bn3L.jpg)
## LNbits as an insta-wallet
Wallets can be easily generated and given out to people at events (one click multi-wallet generation to be added soon).
"Go to this website", has a lot less friction than "Download this app".
![lnurl ATM](https://i.imgur.com/xFWDnwy.png)
# Running LNbits locally
Download this repo
## Running LNbits locally
LNbits uses [Flask](http://flask.pocoo.org/).
Feel free to contribute to the project.
See the [development docs](docs/devs/installation.md) for details on installation and setup.
Application dependencies
------------------------
The application uses [Pipenv][pipenv] to manage Python packages.
While in development, you will need to install all dependencies:
## Tip me
$ pipenv shell
$ pipenv install --dev
If you like this project and might even use or extend it, why not [send some tip love](https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
You will need to set the variables in .env.example, and rename the file to .env
![lnurl ATM](https://i.imgur.com/ri2zOe8.png)
Running the server
------------------
$ flask migrate
$ flask run
There is an environment variable called `FLASK_ENV` that has to be set to `development`
if you want to run Flask in debug mode with autoreload
[pipenv]: https://docs.pipenv.org/#install-pipenv-today
# Tip me
If you like this project and might even use or extend it, why not send some tip love!
https://paywall.link/to/f4e4e
[github-actions]: https://github.com/lnbits/lnbits/actions
[github-actions-badge]: https://github.com/lnbits/lnbits/workflows/test%20suite/badge.svg
[codecov]: https://codecov.io/gh/lnbits/lnbits
[codecov-badge]: https://codecov.io/gh/lnbits/lnbits/branch/master/graph/badge.svg
[license-badge]: https://img.shields.io/badge/license-MIT-blue.svg
+5
View File
@@ -0,0 +1,5 @@
_site
.sass-cache
.jekyll-cache
.jekyll-metadata
vendor
+1
View File
@@ -0,0 +1 @@
lnbits.org
+11
View File
@@ -0,0 +1,11 @@
title: "LNbits docs"
remote_theme: pmarsceill/just-the-docs
logo: "/logos/lnbits-full.png"
search_enabled: true
url: https://lnbits.org
aux_links:
"LNbits on GitHub":
- "//github.com/lnbits/lnbits"
"lnbits.com":
- "//lnbits.com"
+12
View File
@@ -0,0 +1,12 @@
---
layout: default
parent: For developers
title: API reference
nav_order: 3
---
API reference
=============
Coming soon...
+12
View File
@@ -0,0 +1,12 @@
---
layout: default
title: For developers
nav_order: 3
has_children: true
---
For developers
==============
Thanks for contributing :)
+24
View File
@@ -0,0 +1,24 @@
---
layout: default
parent: For developers
title: Making extensions
nav_order: 2
---
Making extensions
=================
Start off by copying the example extension in `lnbits/extensions/example` into your own:
```sh
cp lnbits/extensions/example lnbits/extensions/mysuperplugin -r # Let's not use dashes or anything; it doesn't like those.
cd lnbits/extensions/mysuperplugin
find . -type f -print0 | xargs -0 sed -i 's/example/mysuperplugin/g' # Change all occurrences of 'example' to your plugin name 'mysuperplugin'.
```
Going over the example extension's structure:
* views_api.py: This is where your public API would go. It will be exposed at "$DOMAIN/$PLUGIN/$ROUTE". For example: https://lnbits.com/mysuperplugin/api/v1/tools.
* views.py: The `/` path will show up as your plugin's home page in lnbits' UI. Other pages you can define yourself. The `templates` folder should explain itself in relation to this.
* migrations.py: Create database tables for your plugin. They'll be created when you run `pipenv run flask migrate`.
... This document is a work-in-progress. Send pull requests if you get stuck, so others don't.
+54
View File
@@ -0,0 +1,54 @@
---
layout: default
parent: For developers
title: Installation
nav_order: 1
---
Installation
============
Download this repo and install the dependencies.
Application dependencies
------------------------
The application uses [Pipenv][pipenv] to manage Python packages.
While in development, you will need to install all dependencies:
```sh
$ pipenv shell
$ pipenv install --dev
```
If you wish to use a version of Python higher than 3.7:
```sh
$ pipenv --python 3.8 install --dev
```
You will need to set the variables in `.env.example`, and rename the file to `.env`.
![Files](https://i.imgur.com/ri2zOe8.png)
You might also need to install additional packages, depending on the [backend wallets](../guide/wallets.md) you configured.
E.g. when you want to use LND you have to `pipenv install lnd-grpc`.
Take a look at [Polar](https://lightningpolar.com/) for an excellent way of spinning up a Lightning Network dev environment.
Running the server
------------------
LNbits uses [Flask](http://flask.pocoo.org/) as an application server.
```sh
$ pipenv run flask migrate
$ pipenv run flask run
```
There is an environment variable called `FLASK_ENV` that has to be set to `development`
if you want to run Flask in debug mode with autoreload
[pipenv]: https://pipenv.pypa.io/
Frontend
--------
The views are build using [Vue.js and Quasar](https://quasar.dev/start/how-to-use-vue).
+79
View File
@@ -0,0 +1,79 @@
---
layout: default
title: Backend wallets
nav_order: 2
---
Backend wallets
===============
LNbits can run on top of many lightning-network funding sources. Currently there is support for
CLightning, LND, LNbits, LNPay, lntxbot and OpenNode, with more being added regularily.
A backend wallet can be configured using the following LNbits environment variables:
### CLightning
Using this wallet requires the installation of the `pylightning` Python package.
- `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet**
- `CLIGHTNING_RPC`: /file/path/lightning-rpc
### LND (gRPC)
Using this wallet requires the installation of the `lnd-grpc` Python package.
- `LNBITS_BACKEND_WALLET_CLASS`: **LndWallet**
- `LND_GRPC_ENDPOINT`: ip_address
- `LND_GRPC_PORT`: port
- `LND_CERT`: /file/path/tls.cert
- `LND_ADMIN_MACAROON`: /file/path/admin.macaroon
- `LND_INVOICE_MACAROON`: /file/path/invoice.macaroon
- `LND_READ_MACAROON`: /file/path/read.macaroon
### LND (REST)
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
- `LND_REST_ENDPOINT`: ip_address
- `LND_CERT`: /file/path/tls.cert
- `LND_ADMIN_MACAROON`: /file/path/admin.macaroon
- `LND_INVOICE_MACAROON`: /file/path/invoice.macaroon
- `LND_READ_MACAROON`: /file/path/read.macaroon
### LNbits
- `LNBITS_BACKEND_WALLET_CLASS`: **LNbitsWallet**
- `LNBITS_ENDPOINT`: ex. https://lnbits.com
- `LNBITS_ADMIN_KEY`: apiKey
- `LNBITS_INVOICE_KEY`: apiKey
### LNPay
- `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet**
- `LNPAY_API_ENDPOINT`: ex. https://lnpay.co/v1/
- `LNPAY_API_KEY`: apiKey
- `LNPAY_ADMIN_KEY`: apiKey
- `LNPAY_INVOICE_KEY`: apiKey
- `LNPAY_READ_KEY`: apiKey
### lntxbot
- `LNBITS_BACKEND_WALLET_CLASS`: **LntxbotWallet**
- `LNTXBOT_API_ENDPOINT`: ex. https://lntxbot.bigsun.xyz/
- `LNTXBOT_ADMIN_KEY`: apiKey
- `LNTXBOT_INVOICE_KEY`: apiKey
### OpenNode
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
- `OPENNODE_API_ENDPOINT`: ex. https://api.opennode.com/
- `OPENNODE_ADMIN_KEY`: apiKey
- `OPENNODE_INVOICE_KEY`: apiKey
+24
View File
@@ -0,0 +1,24 @@
---
layout: default
title: Users Guide
nav_order: 1
---
LNbits, free and open-source lightning-network wallet/accounts system
=====================================================================
LNbits is a very simple Python application that sits on top of any funding source, and can be used as:
* Accounts system to mitigate the risk of exposing applications to your full balance, via unique API keys for each wallet
* Extendable platform for exploring lightning-network functionality via LNbits extension framework
* Part of a development stack via LNbits API
* Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations
LNbits as an account system
---------------------------
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending,
export to csv + more to come...
+2 -4
View File
@@ -3,6 +3,7 @@ import importlib
from flask import Flask
from flask_assets import Environment, Bundle # type: ignore
from flask_compress import Compress # type: ignore
from flask_cors import CORS # type: ignore
from flask_talisman import Talisman # type: ignore
from os import getenv
from werkzeug.middleware.proxy_fix import ProxyFix
@@ -23,6 +24,7 @@ valid_extensions = [ext for ext in ExtensionManager(disabled=disabled_extensions
# -----------------------
Compress(app)
CORS(app)
Talisman(
app,
force_https=FORCE_HTTPS,
@@ -33,10 +35,6 @@ Talisman(
"'unsafe-inline'",
"blob:",
"api.opennode.co",
"fonts.googleapis.com",
"fonts.gstatic.com",
"github.com",
"avatars2.githubusercontent.com",
]
},
)
+9 -4
View File
@@ -114,17 +114,22 @@ def get_wallet(wallet_id: str) -> Optional[Wallet]:
def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]:
with open_db() as db:
check_field = "adminkey" if key_type == "admin" else "inkey"
row = db.fetchone(
f"""
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets
WHERE {check_field} = ?
WHERE adminkey = ? OR inkey = ?
""",
(key,),
(key, key),
)
return Wallet(**row) if row else None
if not row:
return None
if key_type == "admin" and row["adminkey"] != key:
return None
return Wallet(**row)
# wallet payments
+1 -1
View File
@@ -1,4 +1,4 @@
new Vue({
el: '#vue',
mixins: [windowMixin]
});
})
+6 -6
View File
@@ -3,23 +3,23 @@ new Vue({
mixins: [windowMixin],
data: function () {
return {
disclaimerDialog: {
disclaimerDialog: {
show: false,
data: {}
},
},
walletName: ''
};
}
},
methods: {
createWallet: function () {
LNbits.href.createWallet(this.walletName);
LNbits.href.createWallet(this.walletName)
},
processing: function () {
this.$q.notify({
timeout: 0,
message: 'Processing...',
icon: null
});
})
}
}
});
})
+200 -142
View File
@@ -1,39 +1,49 @@
Vue.component(VueQrcode.name, VueQrcode);
Vue.use(VueQrcodeReader);
Vue.component(VueQrcode.name, VueQrcode)
Vue.use(VueQrcodeReader)
function generateChart(canvas, payments) {
var txs = [];
var n = 0;
var txs = []
var n = 0
var data = {
labels: [],
income: [],
outcome: [],
cumulative: []
};
}
_.each(payments.slice(0).sort(function (a, b) {
return a.time - b.time;
}), function (tx) {
txs.push({
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
sat: tx.sat,
});
});
_.each(
payments.slice(0).sort(function (a, b) {
return a.time - b.time
}),
function (tx) {
txs.push({
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
sat: tx.sat
})
}
)
_.each(_.groupBy(txs, 'hour'), function (value, day) {
var income = _.reduce(value, function(memo, tx) {
return (tx.sat >= 0) ? memo + tx.sat : memo;
}, 0);
var outcome = _.reduce(value, function(memo, tx) {
return (tx.sat < 0) ? memo + Math.abs(tx.sat) : memo;
}, 0);
n = n + income - outcome;
data.labels.push(day);
data.income.push(income);
data.outcome.push(outcome);
data.cumulative.push(n);
});
var income = _.reduce(
value,
function (memo, tx) {
return tx.sat >= 0 ? memo + tx.sat : memo
},
0
)
var outcome = _.reduce(
value,
function (memo, tx) {
return tx.sat < 0 ? memo + Math.abs(tx.sat) : memo
},
0
)
n = n + income - outcome
data.labels.push(day)
data.income.push(income)
data.outcome.push(outcome)
data.cumulative.push(n)
})
new Chart(canvas.getContext('2d'), {
type: 'bar',
@@ -44,7 +54,7 @@ function generateChart(canvas, payments) {
data: data.cumulative,
type: 'line',
label: 'balance',
backgroundColor: '#673ab7', // deep-purple
backgroundColor: '#673ab7', // deep-purple
borderColor: '#673ab7',
borderWidth: 4,
pointRadius: 3,
@@ -55,14 +65,14 @@ function generateChart(canvas, payments) {
type: 'bar',
label: 'in',
barPercentage: 0.75,
backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // green
backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // green
},
{
data: data.outcome,
type: 'bar',
label: 'out',
barPercentage: 0.75,
backgroundColor: window.Color('rgb(233,30,99)').alpha(0.5).rgbString() // pink
backgroundColor: window.Color('rgb(233,30,99)').alpha(0.5).rgbString() // pink
}
]
},
@@ -72,18 +82,20 @@ function generateChart(canvas, payments) {
},
tooltips: {
mode: 'index',
intersect:false
intersect: false
},
scales: {
xAxes: [{
type: 'time',
display: true,
offset: true,
time: {
minUnit: 'hour',
stepSize: 3
xAxes: [
{
type: 'time',
display: true,
offset: true,
time: {
minUnit: 'hour',
stepSize: 3
}
}
}],
]
},
// performance tweaks
animation: {
@@ -95,10 +107,9 @@ function generateChart(canvas, payments) {
}
}
}
});
})
}
new Vue({
el: '#vue',
mixins: [windowMixin],
@@ -128,8 +139,20 @@ new Vue({
paymentsTable: {
columns: [
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true},
{name: 'sat', align: 'right', label: 'Amount (sat)', field: 'sat', sortable: true}
{
name: 'date',
align: 'left',
label: 'Date',
field: 'date',
sortable: true
},
{
name: 'sat',
align: 'right',
label: 'Amount (sat)',
field: 'sat',
sortable: true
}
],
pagination: {
rowsPerPage: 10
@@ -143,46 +166,50 @@ new Vue({
show: false,
location: window.location
}
};
}
},
computed: {
filteredPayments: function () {
var q = this.paymentsTable.filter;
if (!q || q == '') return this.payments;
var q = this.paymentsTable.filter
if (!q || q == '') return this.payments
return LNbits.utils.search(this.payments, q);
return LNbits.utils.search(this.payments, q)
},
balance: function () {
if (this.payments.length) {
return _.pluck(this.payments, 'amount').reduce(function (a, b) { return a + b; }, 0) / 1000;
return (
_.pluck(this.payments, 'amount').reduce(function (a, b) {
return a + b
}, 0) / 1000
)
}
return this.g.wallet.sat;
return this.g.wallet.sat
},
fbalance: function () {
return LNbits.utils.formatSat(this.balance)
},
canPay: function () {
if (!this.send.invoice) return false;
return this.send.invoice.sat <= this.balance;
if (!this.send.invoice) return false
return this.send.invoice.sat <= this.balance
},
pendingPaymentsExist: function () {
return (this.payments)
return this.payments
? _.where(this.payments, {pending: 1}).length > 0
: false;
: false
}
},
methods: {
closeCamera: function () {
this.sendCamera.show = false;
this.sendCamera.show = false
},
showCamera: function () {
this.sendCamera.show = true;
this.sendCamera.show = true
},
showChart: function () {
this.paymentsChart.show = true;
this.paymentsChart.show = true
this.$nextTick(function () {
generateChart(this.$refs.canvas, this.payments);
});
generateChart(this.$refs.canvas, this.payments)
})
},
showReceiveDialog: function () {
this.receive = {
@@ -194,7 +221,7 @@ new Vue({
memo: ''
},
paymentChecker: null
};
}
},
showSendDialog: function () {
this.send = {
@@ -204,52 +231,64 @@ new Vue({
bolt11: ''
},
paymentChecker: null
};
}
},
closeReceiveDialog: function () {
var checker = this.receive.paymentChecker;
var checker = this.receive.paymentChecker
setTimeout(function () {
clearInterval(checker);
}, 10000);
clearInterval(checker)
}, 10000)
},
closeSendDialog: function () {
this.sendCamera.show = false;
var checker = this.send.paymentChecker;
this.sendCamera.show = false
var checker = this.send.paymentChecker
setTimeout(function () {
clearInterval(checker);
}, 1000);
clearInterval(checker)
}, 1000)
},
createInvoice: function () {
var self = this;
this.receive.status = 'loading';
LNbits.api.createInvoice(this.g.wallet, this.receive.data.amount, this.receive.data.memo)
var self = this
this.receive.status = 'loading'
LNbits.api
.createInvoice(
this.g.wallet,
this.receive.data.amount,
this.receive.data.memo
)
.then(function (response) {
self.receive.status = 'success';
self.receive.paymentReq = response.data.payment_request;
self.receive.status = 'success'
self.receive.paymentReq = response.data.payment_request
self.receive.paymentChecker = setInterval(function () {
LNbits.api.getPayment(self.g.wallet, response.data.checking_id).then(function (response) {
if (response.data.paid) {
self.fetchPayments();
self.receive.show = false;
clearInterval(self.receive.paymentChecker);
}
});
}, 2000);
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
self.receive.status = 'pending';
});
LNbits.api
.getPayment(self.g.wallet, response.data.checking_id)
.then(function (response) {
if (response.data.paid) {
self.fetchPayments()
self.receive.show = false
clearInterval(self.receive.paymentChecker)
}
})
}, 2000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
self.receive.status = 'pending'
})
},
decodeQR: function (res) {
this.send.data.bolt11 = res;
this.decodeInvoice();
this.sendCamera.show = false;
this.send.data.bolt11 = res
this.decodeInvoice()
this.sendCamera.show = false
},
decodeInvoice: function () {
if (this.send.data.bolt11.startsWith('lightning:')) {
this.send.data.bolt11 = this.send.data.bolt11.slice(10)
}
let invoice
try {
var invoice = decode(this.send.data.bolt11);
invoice = decode(this.send.data.bolt11)
} catch (error) {
this.$q.notify({
timeout: 3000,
@@ -257,101 +296,120 @@ new Vue({
message: error + '.',
caption: '400 BAD REQUEST',
icon: null
});
return;
})
return
}
var cleanInvoice = {
let cleanInvoice = {
msat: invoice.human_readable_part.amount,
sat: invoice.human_readable_part.amount / 1000,
fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000)
};
}
_.each(invoice.data.tags, function (tag) {
if (_.isObject(tag) && _.has(tag, 'description')) {
if (tag.description == 'payment_hash') { cleanInvoice.hash = tag.value; }
else if (tag.description == 'description') { cleanInvoice.description = tag.value; }
else if (tag.description == 'expiry') {
var expireDate = new Date((invoice.data.time_stamp + tag.value) * 1000);
cleanInvoice.expireDate = Quasar.utils.date.formatDate(expireDate, 'YYYY-MM-DDTHH:mm:ss.SSSZ');
cleanInvoice.expired = false; // TODO
if (tag.description == 'payment_hash') {
cleanInvoice.hash = tag.value
} else if (tag.description == 'description') {
cleanInvoice.description = tag.value
} else if (tag.description == 'expiry') {
var expireDate = new Date(
(invoice.data.time_stamp + tag.value) * 1000
)
cleanInvoice.expireDate = Quasar.utils.date.formatDate(
expireDate,
'YYYY-MM-DDTHH:mm:ss.SSSZ'
)
cleanInvoice.expired = false // TODO
}
}
});
})
this.send.invoice = Object.freeze(cleanInvoice);
this.send.invoice = Object.freeze(cleanInvoice)
},
payInvoice: function () {
var self = this;
var self = this
dismissPaymentMsg = this.$q.notify({
timeout: 0,
message: 'Processing payment...',
icon: null
});
})
LNbits.api.payInvoice(this.g.wallet, this.send.data.bolt11).then(function (response) {
self.send.paymentChecker = setInterval(function () {
LNbits.api.getPayment(self.g.wallet, response.data.checking_id).then(function (res) {
if (res.data.paid) {
self.send.show = false;
clearInterval(self.send.paymentChecker);
dismissPaymentMsg();
self.fetchPayments();
}
});
}, 2000);
}).catch(function (error) {
dismissPaymentMsg();
LNbits.utils.notifyApiError(error);
});
LNbits.api
.payInvoice(this.g.wallet, this.send.data.bolt11)
.then(function (response) {
self.send.paymentChecker = setInterval(function () {
LNbits.api
.getPayment(self.g.wallet, response.data.checking_id)
.then(function (res) {
if (res.data.paid) {
self.send.show = false
clearInterval(self.send.paymentChecker)
dismissPaymentMsg()
self.fetchPayments()
}
})
}, 2000)
})
.catch(function (error) {
dismissPaymentMsg()
LNbits.utils.notifyApiError(error)
})
},
deleteWallet: function (walletId, user) {
LNbits.utils.confirmDialog(
'Are you sure you want to delete this wallet?'
).onOk(function () {
LNbits.href.deleteWallet(walletId, user);
});
LNbits.utils
.confirmDialog('Are you sure you want to delete this wallet?')
.onOk(function () {
LNbits.href.deleteWallet(walletId, user)
})
},
fetchPayments: function (checkPending) {
var self = this;
var self = this
return LNbits.api.getPayments(this.g.wallet, checkPending).then(function (response) {
self.payments = response.data.map(function (obj) {
return LNbits.map.payment(obj);
}).sort(function (a, b) {
return b.time - a.time;
});
});
return LNbits.api
.getPayments(this.g.wallet, checkPending)
.then(function (response) {
self.payments = response.data
.map(function (obj) {
return LNbits.map.payment(obj)
})
.sort(function (a, b) {
return b.time - a.time
})
})
},
checkPendingPayments: function () {
var dismissMsg = this.$q.notify({
timeout: 0,
message: 'Checking pending transactions...',
icon: null
});
})
this.fetchPayments(true).then(function () {
dismissMsg();
});
dismissMsg()
})
},
exportCSV: function () {
LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments);
LNbits.utils.exportCSV(this.paymentsTable.columns, this.payments)
}
},
watch: {
'payments': function () {
EventHub.$emit('update-wallet-balance', [this.g.wallet.id, this.balance]);
payments: function () {
EventHub.$emit('update-wallet-balance', [this.g.wallet.id, this.balance])
}
},
created: function () {
this.fetchPayments();
setTimeout(this.checkPendingPayments(), 1200);
this.fetchPayments()
setTimeout(this.checkPendingPayments(), 1200)
},
mounted: function () {
if (this.$refs.disclaimer && !this.$q.localStorage.getItem('lnbits.disclaimerShown')) {
this.disclaimerDialog.show = true;
this.$q.localStorage.set('lnbits.disclaimerShown', true);
if (
this.$refs.disclaimer &&
!this.$q.localStorage.getItem('lnbits.disclaimerShown')
) {
this.disclaimerDialog.show = true
this.$q.localStorage.set('lnbits.disclaimerShown', true)
}
}
});
})
+2 -2
View File
@@ -47,10 +47,10 @@
<div class="row q-mt-md q-gutter-sm">
<q-btn outline
color="grey"
type="a" href="https://github.com/arcbtc/lnbits" target="_blank" rel="noopener">View project in GitHub</q-btn>
type="a" href="https://github.com/lnbits/lnbits" target="_blank" rel="noopener">View project in GitHub</q-btn>
<q-btn outline
color="grey"
type="a" href="https://paywall.link/to/f4e4e" target="_blank" rel="noopener">Donate</q-btn>
type="a" href="https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK" target="_blank" rel="noopener">Donate</q-btn>
</div>
</q-card-section>
</q-card>
+3 -3
View File
@@ -4,21 +4,21 @@
{% block styles %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='vendor/vue-qrcode-reader@2.1.1/vue-qrcode-reader.min.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.css') }}">
{% endblock %}
{% block scripts %}
{{ window_vars(user, wallet) }}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
{% assets filters='rjsmin', output='__bundle__/core/chart.js',
'vendor/moment@2.24.0/moment.min.js',
'vendor/moment@2.25.1/moment.min.js',
'vendor/chart.js@2.9.3/chart.min.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
{% assets filters='rjsmin', output='__bundle__/core/wallet.js',
'vendor/bolt11/utils.js',
'vendor/bolt11/decoder.js',
'vendor/vue-qrcode-reader@2.1.1/vue-qrcode-reader.min.js',
'vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.js',
'core/js/wallet.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
+13 -13
View File
@@ -1,8 +1,8 @@
from flask import g, jsonify, request
from http import HTTPStatus
from lnbits.core import core_app
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.helpers import Status
from lnbits.settings import WALLET
from ..services import create_invoice, pay_invoice
@@ -20,7 +20,7 @@ def api_payments():
else:
payment.set_pending(WALLET.get_invoice_status(payment.checking_id).pending)
return jsonify(g.wallet.get_payments()), Status.OK
return jsonify(g.wallet.get_payments()), HTTPStatus.OK
@api_check_wallet_key("invoice")
@@ -36,9 +36,9 @@ def api_payments_create_invoice():
wallet_id=g.wallet.id, amount=g.data["amount"], memo=g.data["memo"]
)
except Exception as e:
return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.CREATED
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED
@api_check_wallet_key("admin")
@@ -47,13 +47,13 @@ def api_payments_pay_invoice():
try:
checking_id = pay_invoice(wallet_id=g.wallet.id, bolt11=g.data["bolt11"])
except ValueError as e:
return jsonify({"message": str(e)}), Status.BAD_REQUEST
return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST
except PermissionError as e:
return jsonify({"message": str(e)}), Status.FORBIDDEN
return jsonify({"message": str(e)}), HTTPStatus.FORBIDDEN
except Exception as e:
return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return jsonify({"checking_id": checking_id}), Status.CREATED
return jsonify({"checking_id": checking_id}), HTTPStatus.CREATED
@core_app.route("/api/v1/payments", methods=["POST"])
@@ -70,9 +70,9 @@ def api_payment(checking_id):
payment = g.wallet.get_payment(checking_id)
if not payment:
return jsonify({"message": "Payment does not exist."}), Status.NOT_FOUND
return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND
elif not payment.pending:
return jsonify({"paid": True}), Status.OK
return jsonify({"paid": True}), HTTPStatus.OK
try:
if payment.is_out:
@@ -80,10 +80,10 @@ def api_payment(checking_id):
elif payment.is_in:
is_paid = not WALLET.get_invoice_status(checking_id).pending
except Exception:
return jsonify({"paid": False}), Status.OK
return jsonify({"paid": False}), HTTPStatus.OK
if is_paid:
payment.set_pending(False)
return jsonify({"paid": True}), Status.OK
return jsonify({"paid": True}), HTTPStatus.OK
return jsonify({"paid": False}), Status.OK
return jsonify({"paid": False}), HTTPStatus.OK
+10 -6
View File
@@ -1,9 +1,9 @@
from flask import g, abort, redirect, request, render_template, send_from_directory, url_for
from os import path
from http import HTTPStatus
from os import getenv, path
from lnbits.core import core_app
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.helpers import Status
from lnbits.settings import SERVICE_FEE
from ..crud import (
@@ -33,7 +33,7 @@ def extensions():
extension_to_disable = request.args.get("disable", type=str)
if extension_to_enable and extension_to_disable:
abort(Status.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
abort(HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
if extension_to_enable:
update_user_extension(user_id=g.user.id, extension=extension_to_enable, active=1)
@@ -60,7 +60,11 @@ def wallet():
if not user_id:
user = get_user(create_account().id)
else:
user = get_user(user_id) or abort(Status.NOT_FOUND, "User does not exist.")
user = get_user(user_id) or abort(HTTPStatus.NOT_FOUND, "User does not exist.")
allowed_users = getenv("LNBITS_ALLOWED_USERS", "all")
if allowed_users != "all" and user_id not in allowed_users.split(","):
abort(HTTPStatus.UNAUTHORIZED, f"User not authorized.")
if not wallet_id:
if user.wallets and not wallet_name:
@@ -71,7 +75,7 @@ def wallet():
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
if wallet_id not in user.wallet_ids:
abort(Status.FORBIDDEN, "Not your wallet.")
abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
return render_template("core/wallet.html", user=user, wallet=user.get_wallet(wallet_id), service_fee=service_fee)
@@ -84,7 +88,7 @@ def deletewallet():
user_wallet_ids = g.user.wallet_ids
if wallet_id not in user_wallet_ids:
abort(Status.FORBIDDEN, "Not your wallet.")
abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
else:
delete_wallet(user_id=g.user.id, wallet_id=wallet_id)
user_wallet_ids.remove(wallet_id)
+9 -5
View File
@@ -1,12 +1,12 @@
import requests
from flask import abort, redirect, request, url_for
from http import HTTPStatus
from lnurl import LnurlWithdrawResponse, handle as handle_lnurl # type: ignore
from lnurl.exceptions import LnurlException # type: ignore
from time import sleep
from lnbits.core import core_app
from lnbits.helpers import Status
from lnbits.settings import WALLET
from ..crud import create_account, get_user, create_wallet, create_payment
@@ -17,9 +17,13 @@ def lnurlwallet():
memo = "LNbits LNURL funding"
try:
withdraw_res = handle_lnurl(request.args.get("lightning"), response_class=LnurlWithdrawResponse)
withdraw_res = handle_lnurl(request.args.get("lightning"))
if not withdraw_res.ok:
abort(HTTPStatus.BAD_REQUEST, f"Could not process LNURL-withdraw: {withdraw_res.error_msg}")
if not isinstance(withdraw_res, LnurlWithdrawResponse):
abort(HTTPStatus.BAD_REQUEST, "Not a valid LNURL-withdraw.")
except LnurlException:
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process LNURL-withdraw.")
try:
ok, checking_id, payment_request, error_message = WALLET.create_invoice(withdraw_res.max_sats, memo)
@@ -27,7 +31,7 @@ def lnurlwallet():
ok, error_message = False, str(e)
if not ok:
abort(Status.INTERNAL_SERVER_ERROR, error_message)
abort(HTTPStatus.INTERNAL_SERVER_ERROR, error_message)
r = requests.get(
withdraw_res.callback.base,
@@ -35,7 +39,7 @@ def lnurlwallet():
)
if not r.ok:
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process LNURL-withdraw.")
for i in range(10):
invoice_status = WALLET.get_invoice_status(checking_id)
+15 -9
View File
@@ -1,11 +1,12 @@
from cerberus import Validator # type: ignore
from flask import g, abort, jsonify, request
from functools import wraps
from http import HTTPStatus
from os import getenv
from typing import List, Union
from uuid import UUID
from lnbits.core.crud import get_user, get_wallet_for_key
from .helpers import Status
def api_check_wallet_key(key_type: str = "invoice"):
@@ -15,10 +16,10 @@ def api_check_wallet_key(key_type: str = "invoice"):
try:
g.wallet = get_wallet_for_key(request.headers["X-Api-Key"], key_type)
except KeyError:
return jsonify({"message": "`X-Api-Key` header missing."}), Status.BAD_REQUEST
return jsonify({"message": "`X-Api-Key` header missing."}), HTTPStatus.BAD_REQUEST
if not g.wallet:
return jsonify({"message": "Wrong keys."}), Status.UNAUTHORIZED
return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED
return view(**kwargs)
@@ -32,13 +33,13 @@ def api_validate_post_request(*, schema: dict):
@wraps(view)
def wrapped_view(**kwargs):
if "application/json" not in request.headers["Content-Type"]:
return jsonify({"message": "Content-Type must be `application/json`."}), Status.BAD_REQUEST
return jsonify({"message": "Content-Type must be `application/json`."}), HTTPStatus.BAD_REQUEST
v = Validator(schema)
g.data = {key: request.json[key] for key in schema.keys()}
g.data = {key: (request.json[key] if key in request.json else None) for key in schema.keys()}
if not v.validate(g.data):
return jsonify({"message": f"Errors in request data: {v.errors}"}), Status.BAD_REQUEST
return jsonify({"message": f"Errors in request data: {v.errors}"}), HTTPStatus.BAD_REQUEST
return view(**kwargs)
@@ -51,7 +52,12 @@ def check_user_exists(param: str = "usr"):
def wrap(view):
@wraps(view)
def wrapped_view(**kwargs):
g.user = get_user(request.args.get(param, type=str)) or abort(Status.NOT_FOUND, "User not found.")
g.user = get_user(request.args.get(param, type=str)) or abort(HTTPStatus.NOT_FOUND, "User does not exist.")
allowed_users = getenv("LNBITS_ALLOWED_USERS", "all")
if allowed_users != "all" and g.user.id not in allowed_users.split(","):
abort(HTTPStatus.UNAUTHORIZED, f"User not authorized.")
return view(**kwargs)
return wrapped_view
@@ -67,13 +73,13 @@ def validate_uuids(params: List[str], *, required: Union[bool, List[str]] = Fals
for param, value in query_params.items():
if not value and (required is True or (required and param in required)):
abort(Status.BAD_REQUEST, f"`{param}` is required.")
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is required.")
if value:
try:
UUID(value, version=version)
except ValueError:
abort(Status.BAD_REQUEST, f"`{param}` is not a valid UUID.")
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is not a valid UUID.")
return view(**kwargs)
@@ -1,16 +1,24 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">Assistant Faucet Milker</h5>
<p>Milking faucets with software, known as "assmilking", seems at first to be black-hat, although in fact there might be some unexplored use cases. An LNURL withdraw gives someone the right to pull funds, which can be done over time. An LNURL withdraw could be used outside of just faucets, to provide money streaming and repeat payments.<br/>Paste or scan an LNURL withdraw, enter the amount for the AMilk to pull and the frequency for it to be pulled.<br/>
<small> Created by, <a href="https://github.com/benarc">Ben Arc</a></small></p>
</q-card>
</q-card-section>
</q-card-section></q-expansion-item>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">Assistant Faucet Milker</h5>
<p>
Milking faucets with software, known as "assmilking", seems at first to
be black-hat, although in fact there might be some unexplored use cases.
An LNURL withdraw gives someone the right to pull funds, which can be
done over time. An LNURL withdraw could be used outside of just faucets,
to provide money streaming and repeat payments.<br />Paste or scan an
LNURL withdraw, enter the amount for the AMilk to pull and the frequency
for it to be pulled.<br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
+232 -210
View File
@@ -1,230 +1,252 @@
{% extends "base.html" %}
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="amilkDialog.show = true"
>New AMilk</q-btn
>
</q-card-section>
</q-card>
{% from "macros.jinja" import window_vars with context %}
{% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="amilkDialog.show = true">New AMilk</q-btn>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">AMilks</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">AMilks</h5>
</div>
<q-table dense flat
:data="amilks"
row-key="id"
:columns="amilksTable.columns"
:pagination.sync="amilksTable.pagination">
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn flat dense size="xs" @click="deleteAMilk(props.row.id)" icon="cancel" color="pink"></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits Assistant Faucet Milker Extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "amilk/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="amilkDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="createAMilk" class="q-gutter-md">
<q-select filled dense emit-value v-model="amilkDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *">
</q-select>
<q-input filled dense
v-model.trim="amilkDialog.data.lnurl"
type="url"
label="LNURL Withdraw"></q-input>
<q-input filled dense
v-model.number="amilkDialog.data.amount"
type="number"
label="Amount *"></q-input>
<q-input filled dense
v-model.trim="amilkDialog.data.atime"
type="number"
label="Hit frequency (secs)"
placeholder="Frequency to be hit"></q-input>
<q-btn unelevated
color="deep-purple"
:disable="amilkDialog.data.amount == null || amilkDialog.data.amount < 0 || amilkDialog.data.lnurl == null"
type="submit">Create amilk</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form>
</q-card>
</q-dialog>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:data="amilks"
row-key="id"
:columns="amilksTable.columns"
:pagination.sync="amilksTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteAMilk(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
{% endblock %}
{% block scripts %}
{{ window_vars(user) }}
<script>
var mapAMilk = function (obj) {
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm');
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount);
obj.wall = ['/amilk/', obj.id].join('');
return obj;
}
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
LNbits Assistant Faucet Milker Extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "amilk/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
amilks: [],
amilksTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'lnurl', align: 'left', label: 'LNURL', field: 'lnurl'},
{name: 'atime', align: 'left', label: 'Freq', field: 'atime'},
{name: 'amount', align: 'left', label: 'Amount', field: 'amount'}
],
pagination: {
rowsPerPage: 10
}
},
amilkDialog: {
show: false,
data: {}
<q-dialog v-model="amilkDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="createAMilk" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="amilkDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
v-model.trim="amilkDialog.data.lnurl"
type="url"
label="LNURL Withdraw"
></q-input>
<q-input
filled
dense
v-model.number="amilkDialog.data.amount"
type="number"
label="Amount *"
></q-input>
<q-input
filled
dense
v-model.trim="amilkDialog.data.atime"
type="number"
label="Hit frequency (secs)"
placeholder="Frequency to be hit"
></q-input>
<q-btn
unelevated
color="deep-purple"
:disable="amilkDialog.data.amount == null || amilkDialog.data.amount < 0 || amilkDialog.data.lnurl == null"
type="submit"
>Create amilk</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapAMilk = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.wall = ['/amilk/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
amilks: [],
amilksTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'lnurl', align: 'left', label: 'LNURL', field: 'lnurl'},
{name: 'atime', align: 'left', label: 'Freq', field: 'atime'},
{name: 'amount', align: 'left', label: 'Amount', field: 'amount'}
],
pagination: {
rowsPerPage: 10
}
};
},
methods: {
},
amilkDialog: {
show: false,
data: {}
}
}
},
methods: {
getAMilks: function () {
var self = this
getAMilks: function () {
var self = this;
LNbits.api.request(
LNbits.api
.request(
'GET',
'/amilk/api/v1/amilk?all_wallets',
this.g.user.wallets[0].inkey
).then(function (response) {
)
.then(function (response) {
self.amilks = response.data.map(function (obj) {
response.data.forEach(MILK);
function MILK(item){
window.setInterval(function(){
LNbits.api.request(
'GET',
'/amilk/api/v1/amilk/milk/' + item.id,
"Lorem"
).then(function (response) {
self.amilks = response.data.map(function (obj) {
return mapAMilk(obj);
});
});
},
item.atime*1000);
response.data.forEach(MILK)
function MILK(item) {
window.setInterval(function () {
LNbits.api
.request(
'GET',
'/amilk/api/v1/amilk/milk/' + item.id,
'Lorem'
)
.then(function (response) {
self.amilks = response.data.map(function (obj) {
return mapAMilk(obj)
})
})
}, item.atime * 1000)
}
return mapAMilk(obj);
});
});
},
createAMilk: function () {
var data = {
lnurl: this.amilkDialog.data.lnurl,
atime: parseInt(this.amilkDialog.data.atime),
amount: this.amilkDialog.data.amount
};
var self = this;
return mapAMilk(obj)
})
})
},
createAMilk: function () {
var data = {
lnurl: this.amilkDialog.data.lnurl,
atime: parseInt(this.amilkDialog.data.atime),
amount: this.amilkDialog.data.amount
}
var self = this
console.log(this.amilkDialog.data.wallet);
console.log(this.amilkDialog.data.wallet)
LNbits.api.request(
LNbits.api
.request(
'POST',
'/amilk/api/v1/amilk',
_.findWhere(this.g.user.wallets, {id: this.amilkDialog.data.wallet}).inkey,
_.findWhere(this.g.user.wallets, {id: this.amilkDialog.data.wallet})
.inkey,
data
).then(function (response) {
self.amilks.push(mapAMilk(response.data));
self.amilkDialog.show = false;
self.amilkDialog.data = {};
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
},
deleteAMilk: function (amilkId) {
var self = this;
var amilk = _.findWhere(this.amilks, {id: amilkId});
LNbits.utils.confirmDialog(
'Are you sure you want to delete this AMilk link?'
).onOk(function () {
LNbits.api.request(
'DELETE',
'/amilk/api/v1/amilks/' + amilkId,
_.findWhere(self.g.user.wallets, {id: amilk.wallet}).inkey
).then(function (response) {
self.amilks = _.reject(self.amilks, function (obj) { return obj.id == amilkId; });
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
});
},
exportCSV: function () {
LNbits.utils.exportCSV(this.amilksTable.columns, this.amilks);
}
)
.then(function (response) {
self.amilks.push(mapAMilk(response.data))
self.amilkDialog.show = false
self.amilkDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
created: function () {
if (this.g.user.wallets.length) {
this.getAMilks();
}
deleteAMilk: function (amilkId) {
var self = this
var amilk = _.findWhere(this.amilks, {id: amilkId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this AMilk link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/amilk/api/v1/amilks/' + amilkId,
_.findWhere(self.g.user.wallets, {id: amilk.wallet}).inkey
)
.then(function (response) {
self.amilks = _.reject(self.amilks, function (obj) {
return obj.id == amilkId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportCSV: function () {
LNbits.utils.exportCSV(this.amilksTable.columns, this.amilks)
}
});
</script>
},
created: function () {
if (this.g.user.wallets.length) {
this.getAMilks()
}
}
})
</script>
{% endblock %}
+2 -2
View File
@@ -1,8 +1,8 @@
from flask import g, abort, render_template
from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.amilk import amilk_ext
from lnbits.helpers import Status
from .crud import get_amilk
@@ -16,6 +16,6 @@ def index():
@amilk_ext.route("/<amilk_id>")
def wall(amilk_id):
amilk = get_amilk(amilk_id) or abort(Status.NOT_FOUND, "AMilk does not exist.")
amilk = get_amilk(amilk_id) or abort(HTTPStatus.NOT_FOUND, "AMilk does not exist.")
return render_template("amilk/wall.html", amilk=amilk)
+12 -12
View File
@@ -1,8 +1,8 @@
from flask import g, jsonify, request
from http import HTTPStatus
from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.helpers import Status
from lnbits.extensions.amilk import amilk_ext
from .crud import create_amilk, get_amilk, get_amilks, delete_amilk
@@ -23,7 +23,7 @@ def api_amilks():
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([amilk._asdict() for amilk in get_amilks(wallet_ids)]), Status.OK
return jsonify([amilk._asdict() for amilk in get_amilks(wallet_ids)]), HTTPStatus.OK
@amilk_ext.route("/api/v1/amilk/milk/<amilk_id>", methods=["GET"])
@@ -34,10 +34,10 @@ def api_amilkit(amilk_id):
try:
withdraw_res = handle_lnurl(milk.lnurl, response_class=LnurlWithdrawResponse)
except LnurlException:
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
print(withdraw_res.max_sats)
try:
checking_id, payment_request = create_invoice(wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo)
#print(payment_request)
@@ -48,10 +48,10 @@ def api_amilkit(amilk_id):
withdraw_res.callback.base,
params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": payment_request}},
)
if not r.ok:
abort(Status.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
for i in range(10):
invoice_status = WALLET.get_invoice_status(checking_id)
@@ -59,10 +59,10 @@ def api_amilkit(amilk_id):
if not invoice_status.paid:
continue
else:
return jsonify({"paid": False}), Status.OK
return jsonify({"paid": False}), HTTPStatus.OK
break
return jsonify({"paid": True}), Status.OK
return jsonify({"paid": True}), HTTPStatus.OK
@amilk_ext.route("/api/v1/amilk", methods=["POST"])
@@ -75,7 +75,7 @@ def api_amilkit(amilk_id):
def api_amilk_create():
amilk = create_amilk(wallet_id=g.wallet.id, lnurl=g.data["lnurl"], atime=g.data["atime"], amount=g.data["amount"])
return jsonify(amilk._asdict()), Status.CREATED
return jsonify(amilk._asdict()), HTTPStatus.CREATED
@amilk_ext.route("/api/v1/amilk/<amilk_id>", methods=["DELETE"])
@@ -84,11 +84,11 @@ def api_amilk_delete(amilk_id):
amilk = get_amilk(amilk_id)
if not amilk:
return jsonify({"message": "Paywall does not exist."}), Status.NOT_FOUND
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
if amilk.wallet != g.wallet.id:
return jsonify({"message": "Not your amilk."}), Status.FORBIDDEN
return jsonify({"message": "Not your amilk."}), HTTPStatus.FORBIDDEN
delete_amilk(amilk_id)
return "", Status.NO_CONTENT
return "", HTTPStatus.NO_CONTENT
@@ -2,5 +2,5 @@
"name": "Diagon Alley",
"short_description": "Movable anonymous market stand",
"icon": "add_shopping_cart",
"contributors": ["eillarra"]
"contributors": ["benarc"]
}
@@ -1,19 +1,29 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">Diagon Alley: Movable, anonymous market stall</h5>
<p>Make a list of products to sell, point your list of products at a public indexer. Buyers browse your products on the indexer, and pay you directly. Ratings are managed by the indexer. Your stall can be listed in multiple indexers, even over TOR, if you wish to be anonymous.<br/>
<small> Created by, <a href="https://github.com/benarc">Ben Arc</a></small></p>
</q-card>
</q-card-section>
</q-card-section>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">
Diagon Alley: Decentralised Market-Stalls
</h5>
<p>
Make a list of products to sell, point your list of products at a public
indexer. Buyers browse your products on the indexer, and pay you
directly. Ratings are managed by the indexer. Your stall can be listed
in multiple indexers, even over TOR, if you wish to be anonymous.<br />
More information on the
<a href="https://github.com/lnbits/Diagon-Alley"
>Diagon Alley Protocol</a
><br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="extras"
@@ -21,43 +31,91 @@
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="Get prodcuts, categorised by wallet">
<q-expansion-item
group="api"
dense
expand-separator
label="Get prodcuts, categorised by wallet"
>
<q-card>
<q-card-section>
<code><span class="text-light-blue">GET</span> /api/v1/diagonalley/stall/products/&lt;indexer_id&gt;</code>
<code
><span class="text-light-blue">GET</span>
/api/v1/diagonalley/stall/products/&lt;indexer_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>Product JSON list</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X GET {{ request.url_root }}diagonalley/api/v1/diagonalley/stall/products/&lt;indexer_id&gt;</code>
<code
>curl -X GET {{ request.url_root
}}diagonalley/api/v1/diagonalley/stall/products/&lt;indexer_id&gt;</code
>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Get invoice for product">
<q-expansion-item
group="api"
dense
expand-separator
label="Get invoice for product"
>
<q-card>
<q-card-section>
<code><span class="text-light-green">POST</span> /api/v1/diagonalley/stall/order/&lt;indexer_id&gt;</code>
<code
><span class="text-light-green">POST</span>
/api/v1/diagonalley/stall/order/&lt;indexer_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"id": &lt;string&gt;, "address": &lt;string&gt;, "shippingzone": &lt;integer&gt;, "email": &lt;string&gt;, "quantity": &lt;integer&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5>
<code>{"checking_id": &lt;string&gt;,"payment_request": &lt;string&gt;}</code>
<code
>{"id": &lt;string&gt;, "address": &lt;string&gt;, "shippingzone":
&lt;integer&gt;, "email": &lt;string&gt;, "quantity":
&lt;integer&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"checking_id": &lt;string&gt;,"payment_request":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X POST {{ request.url_root }}diagonalley/api/v1/diagonalley/stall/order/&lt;indexer_id&gt; -d '{"id": &lt;product_id&&gt;, "email": &lt;customer_email&gt;, "address": &lt;customer_address&gt;, "quantity": 2, "shippingzone": 1}' -H "Content-type: application/json"
</code>
<code
>curl -X POST {{ request.url_root
}}diagonalley/api/v1/diagonalley/stall/order/&lt;indexer_id&gt; -d
'{"id": &lt;product_id&&gt;, "email": &lt;customer_email&gt;,
"address": &lt;customer_address&gt;, "quantity": 2, "shippingzone":
1}' -H "Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Check a product has been shipped"
class="q-mb-md">
<q-expansion-item
group="api"
dense
expand-separator
label="Check a product has been shipped"
class="q-mb-md"
>
<q-card>
<q-card-section>
<code><span class="text-light-blue">GET</span> /diagonalley/api/v1/diagonalley/stall/checkshipped/&lt;checking_id&gt;</code>
<code
><span class="text-light-blue">GET</span>
/diagonalley/api/v1/diagonalley/stall/checkshipped/&lt;checking_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Returns 200 OK (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{"shipped": &lt;boolean&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X GET {{ request.url_root }}diagonalley/api/v1/diagonalley/stall/checkshipped/&lt;checking_id&gt; -H "Content-type: application/json"</code>
<code
>curl -X GET {{ request.url_root
}}diagonalley/api/v1/diagonalley/stall/checkshipped/&lt;checking_id&gt;
-H "Content-type: application/json"</code
>
</q-card-section>
</q-card>
</q-expansion-item>
File diff suppressed because it is too large Load Diff
@@ -1 +1,3 @@
<script>console.log("{{ stall }}")</script>
<script>
console.log('{{ stall }}')
</script>
+3 -2
View File
@@ -1,8 +1,9 @@
from flask import g, abort, render_template, jsonify
import json
from flask import g, abort, render_template, jsonify
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.diagonalley import diagonalley_ext
from lnbits.helpers import Status
from lnbits.db import open_ext_db
+27 -30
View File
@@ -1,8 +1,8 @@
from flask import g, jsonify, request
from http import HTTPStatus
from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.helpers import Status
from lnbits.extensions.diagonalley import diagonalley_ext
from .crud import create_diagonalleys_product,get_diagonalleys_product,get_diagonalleys_products,delete_diagonalleys_product,create_diagonalleys_indexer,update_diagonalleys_indexer,get_diagonalleys_indexer,get_diagonalleys_indexers,delete_diagonalleys_indexer,create_diagonalleys_order,get_diagonalleys_order,get_diagonalleys_orders,delete_diagonalleys_order
@@ -21,7 +21,7 @@ def api_diagonalley_products():
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([product._asdict() for product in get_diagonalleys_products(wallet_ids)]), Status.OK
return jsonify([product._asdict() for product in get_diagonalleys_products(wallet_ids)]), HTTPStatus.OK
@diagonalley_ext.route("/api/v1/diagonalley/products", methods=["POST"])
@@ -41,16 +41,16 @@ def api_diagonalley_product_create(product_id=None):
product = get_diagonalleys_indexer(product_id)
if not product:
return jsonify({"message": "Withdraw product does not exist."}), Status.NOT_FOUND
return jsonify({"message": "Withdraw product does not exist."}), HTTPStatus.NOT_FOUND
if product.wallet != g.wallet.id:
return jsonify({"message": "Not your withdraw product."}), Status.FORBIDDEN
return jsonify({"message": "Not your withdraw product."}), HTTPStatus.FORBIDDEN
product = update_diagonalleys_product(product_id, **g.data)
else:
product = create_diagonalleys_product(wallet_id=g.wallet.id, **g.data)
return jsonify(product._asdict()), Status.OK if product_id else Status.CREATED
return jsonify(product._asdict()), HTTPStatus.OK if product_id else HTTPStatus.CREATED
@@ -60,14 +60,14 @@ def api_diagonalley_products_delete(product_id):
product = get_diagonalleys_product(product_id)
if not product:
return jsonify({"message": "Product does not exist."}), Status.NOT_FOUND
return jsonify({"message": "Product does not exist."}), HTTPStatus.NOT_FOUND
if product.wallet != g.wallet.id:
return jsonify({"message": "Not your Diagon Alley."}), Status.FORBIDDEN
return jsonify({"message": "Not your Diagon Alley."}), HTTPStatus.FORBIDDEN
delete_diagonalleys_product(product_id)
return "", Status.NO_CONTENT
return "", HTTPStatus.NO_CONTENT
@@ -81,7 +81,7 @@ def api_diagonalley_indexers():
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([indexer._asdict() for indexer in get_diagonalleys_indexers(wallet_ids)]), Status.OK
return jsonify([indexer._asdict() for indexer in get_diagonalleys_indexers(wallet_ids)]), HTTPStatus.OK
@diagonalley_ext.route("/api/v1/diagonalley/indexers", methods=["POST"])
@@ -102,16 +102,16 @@ def api_diagonalley_indexer_create(indexer_id=None):
indexer = get_diagonalleys_indexer(indexer_id)
if not indexer:
return jsonify({"message": "Withdraw indexer does not exist."}), Status.NOT_FOUND
return jsonify({"message": "Withdraw indexer does not exist."}), HTTPStatus.NOT_FOUND
if indexer.wallet != g.wallet.id:
return jsonify({"message": "Not your withdraw indexer."}), Status.FORBIDDEN
return jsonify({"message": "Not your withdraw indexer."}), HTTPStatus.FORBIDDEN
indexer = update_diagonalleys_indexer(indexer_id, **g.data)
else:
indexer = create_diagonalleys_indexer(wallet_id=g.wallet.id, **g.data)
return jsonify(indexer._asdict()), Status.OK if indexer_id else Status.CREATED
return jsonify(indexer._asdict()), HTTPStatus.OK if indexer_id else HTTPStatus.CREATED
@diagonalley_ext.route("/api/v1/diagonalley/indexers/<indexer_id>", methods=["DELETE"])
@@ -120,14 +120,14 @@ def api_diagonalley_indexer_delete(indexer_id):
indexer = get_diagonalleys_indexer(indexer_id)
if not indexer:
return jsonify({"message": "Indexer does not exist."}), Status.NOT_FOUND
return jsonify({"message": "Indexer does not exist."}), HTTPStatus.NOT_FOUND
if indexer.wallet != g.wallet.id:
return jsonify({"message": "Not your Indexer."}), Status.FORBIDDEN
return jsonify({"message": "Not your Indexer."}), HTTPStatus.FORBIDDEN
delete_diagonalleys_indexer(indexer_id)
return "", Status.NO_CONTENT
return "", HTTPStatus.NO_CONTENT
###Orders
@@ -140,7 +140,7 @@ def api_diagonalley_orders():
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([order._asdict() for order in get_diagonalleys_orders(wallet_ids)]), Status.OK
return jsonify([order._asdict() for order in get_diagonalleys_orders(wallet_ids)]), HTTPStatus.OK
@diagonalley_ext.route("/api/v1/diagonalley/orders", methods=["POST"])
@@ -154,7 +154,7 @@ def api_diagonalley_orders():
})
def api_diagonalley_order_create():
order = create_diagonalleys_order(wallet_id=g.wallet.id, **g.data)
return jsonify(order._asdict()), Status.CREATED
return jsonify(order._asdict()), HTTPStatus.CREATED
@diagonalley_ext.route("/api/v1/diagonalley/orders/<order_id>", methods=["DELETE"])
@@ -163,14 +163,14 @@ def api_diagonalley_order_delete(order_id):
order = get_diagonalleys_order(order_id)
if not order:
return jsonify({"message": "Indexer does not exist."}), Status.NOT_FOUND
return jsonify({"message": "Indexer does not exist."}), HTTPStatus.NOT_FOUND
if order.wallet != g.wallet.id:
return jsonify({"message": "Not your Indexer."}), Status.FORBIDDEN
return jsonify({"message": "Not your Indexer."}), HTTPStatus.FORBIDDEN
delete_diagonalleys_indexer(order_id)
return "", Status.NO_CONTENT
return "", HTTPStatus.NO_CONTENT
@diagonalley_ext.route("/api/v1/diagonalley/orders/paid/<order_id>", methods=["GET"])
@@ -178,7 +178,7 @@ def api_diagonalley_order_delete(order_id):
def api_diagonalleys_order_paid(order_id):
with open_ext_db("diagonalley") as db:
db.execute("UPDATE orders SET paid = ? WHERE id = ?", (True, order_id,))
return "", Status.OK
return "", HTTPStatus.OK
@diagonalley_ext.route("/api/v1/diagonalley/orders/shipped/<order_id>", methods=["GET"])
@@ -188,7 +188,7 @@ def api_diagonalleys_order_shipped(order_id):
db.execute("UPDATE orders SET shipped = ? WHERE id = ?", (True, order_id,))
order = db.fetchone("SELECT * FROM orders WHERE id = ?", (order_id,))
return jsonify([order._asdict() for order in get_diagonalleys_orders(order["wallet"])]), Status.OK
return jsonify([order._asdict() for order in get_diagonalleys_orders(order["wallet"])]), HTTPStatus.OK
###List products based on indexer id
@@ -199,13 +199,13 @@ def api_diagonalleys_stall_products(indexer_id):
rows = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
print(rows[1])
if not rows:
return jsonify({"message": "Indexer does not exist."}), Status.NOT_FOUND
return jsonify({"message": "Indexer does not exist."}), HTTPStatus.NOT_FOUND
products = db.fetchone("SELECT * FROM products WHERE wallet = ?", (rows[1],))
if not products:
return jsonify({"message": "No products"}), Status.NOT_FOUND
return jsonify({"message": "No products"}), HTTPStatus.NOT_FOUND
return jsonify([products._asdict() for products in get_diagonalleys_products(rows[1])]), Status.OK
return jsonify([products._asdict() for products in get_diagonalleys_products(rows[1])]), HTTPStatus.OK
###Check a product has been shipped
@@ -214,7 +214,7 @@ def api_diagonalleys_stall_checkshipped(checking_id):
with open_ext_db("diagonalley") as db:
rows = db.fetchone("SELECT * FROM orders WHERE invoiceid = ?", (checking_id,))
return jsonify({"shipped": rows["shipped"]}), Status.OK
return jsonify({"shipped": rows["shipped"]}), HTTPStatus.OK
###Place order
@@ -245,7 +245,4 @@ def api_diagonalley_stall_order(indexer_id):
""",
(selling_id ,g.data["id"] , product.wallet, product.product, g.data["quantity"], g.data["shippingzone"], g.data["address"], g.data["email"], checking_id, False, False),
)
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.OK
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK
+3 -8
View File
@@ -1,8 +1,3 @@
<h1> LNEVENTS</h1>
<h2>Create/sell tickets for an event</h2>
<p>Events is an easy way to create/sell tickets for an event.
It is advised to setup a specific wallet in lnbits for the event.</p>
<img src="https://i.imgur.com/qHi7ExL.png">
<h1>Events</h1>
<h2>Events: Sell and register event tickets</h2>
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.
+6
View File
@@ -0,0 +1,6 @@
{
"name": "Events",
"short_description": "Sell and register event tickets",
"icon": "local_activity",
"contributors": ["benarc"]
}
@@ -1,6 +0,0 @@
{
"name": "Events",
"short_description": "LN tickets for events.",
"icon": "local_activity",
"contributors": ["arcbtc"]
}
-6
View File
@@ -1,6 +0,0 @@
{
"name": "Events",
"short_description": "LN tickets for events.",
"icon": "local_activity",
"contributors": ["arcbtc"]
}
+142
View File
@@ -0,0 +1,142 @@
from typing import List, Optional, Union
from lnbits.db import open_ext_db
from lnbits.helpers import urlsafe_short_hash
from .models import Tickets, Events
#######TICKETS########
def create_ticket(checking_id: str, wallet: str, event: str, name: str, email: str) -> Tickets:
with open_ext_db("events") as db:
db.execute(
"""
INSERT INTO ticket (id, wallet, event, name, email, registered, paid)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(checking_id, wallet, event, name, email, False, False),
)
return get_ticket(checking_id)
def update_ticket(paid: bool, checking_id: str) -> Tickets:
with open_ext_db("events") as db:
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,))
if row[6] == True:
return get_ticket(checking_id)
db.execute(
"""
UPDATE ticket
SET paid = ?
WHERE id = ?
""",
(paid, checking_id),
)
eventdata = get_event(row[2])
sold = eventdata.sold + 1
amount_tickets = eventdata.amount_tickets - 1
db.execute(
"""
UPDATE events
SET sold = ?, amount_tickets = ?
WHERE id = ?
""",
(sold, amount_tickets, row[2]),
)
return get_ticket(checking_id)
def get_ticket(checking_id: str) -> Optional[Tickets]:
with open_ext_db("events") as db:
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,))
return Tickets(**row) if row else None
def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("events") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM ticket WHERE wallet IN ({q})", (*wallet_ids,))
print("scrum")
return [Tickets(**row) for row in rows]
def delete_ticket(checking_id: str) -> None:
with open_ext_db("events") as db:
db.execute("DELETE FROM ticket WHERE id = ?", (checking_id,))
########EVENTS#########
def create_event(*, wallet: str, name: str, info: str, closing_date: str, event_start_date: str, event_end_date: str, amount_tickets: int, price_per_ticket: int) -> Events:
with open_ext_db("events") as db:
event_id = urlsafe_short_hash()
db.execute(
"""
INSERT INTO events (id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(event_id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, 0),
)
print(event_id)
return get_event(event_id)
def update_event(event_id: str, **kwargs) -> Events:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("events") as db:
db.execute(f"UPDATE events SET {q} WHERE id = ?", (*kwargs.values(), event_id))
row = db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,))
return Events(**row) if row else None
def get_event(event_id: str) -> Optional[Events]:
with open_ext_db("events") as db:
row = db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,))
return Events(**row) if row else None
def get_events(wallet_ids: Union[str, List[str]]) -> List[Events]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("events") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM events WHERE wallet IN ({q})", (*wallet_ids,))
return [Events(**row) for row in rows]
def delete_event(event_id: str) -> None:
with open_ext_db("events") as db:
db.execute("DELETE FROM events WHERE id = ?", (event_id,))
########EVENTTICKETS#########
def get_event_tickets(event_id: str, wallet_id: str) -> Tickets:
with open_ext_db("events") as db:
rows = db.fetchall("SELECT * FROM ticket WHERE wallet = ? AND event = ?", (wallet_id, event_id))
print(rows)
return [Tickets(**row) for row in rows]
def reg_ticket(ticket_id: str) -> Tickets:
with open_ext_db("events") as db:
db.execute("UPDATE ticket SET registered = ? WHERE id = ?", (True, ticket_id))
ticket = db.fetchone("SELECT * FROM ticket WHERE id = ?", (ticket_id,))
print(ticket[1])
rows = db.fetchall("SELECT * FROM ticket WHERE event = ?", (ticket[1],))
return [Tickets(**row) for row in rows]
+89 -1
View File
@@ -1,5 +1,93 @@
from lnbits.db import open_ext_db
def m001_initial(db):
db.execute(
"""
CREATE TABLE IF NOT EXISTS 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 (strftime('%s', 'now'))
);
"""
)
db.execute(
"""
CREATE TABLE IF NOT EXISTS 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 (strftime('%s', 'now'))
);
"""
)
def m002_changed(db):
db.execute(
"""
CREATE TABLE IF NOT EXISTS 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 (strftime('%s', 'now'))
);
"""
)
for row in [list(row) for row in db.fetchall("SELECT * FROM tickets")]:
usescsv = ""
for i in range(row[5]):
if row[7]:
usescsv += "," + str(i + 1)
else:
usescsv += "," + str(1)
usescsv = usescsv[1:]
db.execute(
"""
INSERT INTO ticket (
id,
wallet,
event,
name,
email,
registered,
paid
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
row[0],
row[1],
row[2],
row[3],
row[4],
row[5],
True,
),
)
db.execute("DROP TABLE tickets")
def migrate():
print("pending")
with open_ext_db("events") as db:
m001_initial(db)
m002_changed(db)
+25
View File
@@ -0,0 +1,25 @@
from typing import NamedTuple
class Events(NamedTuple):
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(NamedTuple):
id: str
wallet: str
event: str
name: str
email: str
registered: bool
paid: bool
time: int
-26
View File
@@ -1,26 +0,0 @@
CREATE TABLE IF NOT EXISTS events (
key INTEGER PRIMARY KEY AUTOINCREMENT,
usr TEXT,
wal TEXT,
walnme TEXT,
walinvkey INTEGER,
uni TEXT,
tit TEXT,
cldate TEXT,
notickets INTEGER,
sold INTEGER DEFAULT 0,
prtick INTEGER,
descr TEXT,
unireg TEXT
);
CREATE TABLE IF NOT EXISTS eventssold (
key INTEGER PRIMARY KEY AUTOINCREMENT,
uni TEXT,
email TEXT,
name TEXT,
hash TEXT,
paid INTEGER DEFAULT 0,
reg INTEGER DEFAULT 0
);
@@ -0,0 +1,23 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">
Events: Sell and register ticket waves for an event
</h5>
<p>
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.<br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a>
</small>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -1,570 +1,205 @@
<!-- @format -->
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<h3 class="q-my-none">{{ event_name }}</h3>
<br />
<h5 class="q-my-none">{{ event_info }}</h5>
<br />
<q-form @submit="Invoice()" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialog.data.name"
type="name"
label="Your name "
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.email"
type="email"
label="Your email "
></q-input>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>LNBits Wallet</title>
<meta
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
name="viewport"
/>
<!-- Bootstrap 3.3.2 -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
/>
<!-- FontAwesome 4.3.0 -->
<link
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
rel="stylesheet"
type="text/css"
/>
<!-- Ionicons 2.0.0 -->
<link
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
rel="stylesheet"
type="text/css"
/>
<!-- Theme style -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
/>
<!-- AdminLTE Skins. Choose a skin from the css/skins
folder instead of downloading all of them to reduce the load. -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
/>
<!-- Morris chart -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
/>
<!-- jvectormap -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
/>
<!-- bootstrap wysihtml5 - text editor -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
/>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
<style>
.small-box > .small-box-footer {
text-align: left;
padding-left: 10px;
}
#loadingMessage {
text-align: center;
padding: 40px;
background-color: #eee;
}
#canvas {
width: 100%;
}
#output {
margin-top: 20px;
background: #eee;
padding: 10px;
padding-bottom: 0;
}
#output div {
padding-bottom: 10px;
word-wrap: break-word;
}
#noQRFound {
text-align: center;
}
</style>
<!-- jQuery 2.1.3 -->
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
<!-- jQuery UI 1.11.2 -->
<script
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
type="text/javascript"
></script>
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
<script>
$.widget.bridge('uibutton', $.ui.button)
</script>
<!-- Bootstrap 3.3.2 JS -->
<script
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
type="text/javascript"
></script>
<!-- Morris.js charts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
type="text/javascript"
></script>
<!-- Sparkline -->
<script
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
type="text/javascript"
></script>
<!-- jvectormap -->
<script
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
type="text/javascript"
></script>
<!-- jQuery Knob Chart -->
<script
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
type="text/javascript"
></script>
<!-- Bootstrap WYSIHTML5 -->
<script
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
type="text/javascript"
></script>
<!-- Slimscroll -->
<script
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
type="text/javascript"
></script>
<!-- FastClick -->
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
<!-- AdminLTE App -->
<script
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
type="text/javascript"
></script>
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
<script
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
type="text/javascript"
></script>
<!-- AdminLTE for demo purposes -->
<script
src="{{ url_for('static', filename='dist/js/demo.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
type="text/javascript"
></script>
<link
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
<script
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
type="text/javascript"
></script>
<style>
//GOOFY CSS HACK TO GO DARK
.skin-blue .wrapper {
background:
#1f2234;
}
body {
color: #fff;
}
.skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#1f2234;
border-left-color:#8964a9;
}
.skin-blue .main-header .navbar {
background-color:
#2e507d;
}
.content-wrapper, .right-side {
background-color:
#1f2234;
}
.skin-blue .main-header .logo {
background-color:
#1f2234;
color:
#fff;
}
.skin-blue .sidebar-menu > li.header {
color:
#4b646f;
background:
#1f2234;
}
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > .treeview-menu {
margin: 0 1px;
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > a {
border-left: 3px solid
transparent;
margin-right: 1px;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#3e355a;
border-left-color:#8964a9;
}
.skin-blue .main-header .logo:hover {
background:
#3e355a;
}
.skin-blue .main-header .navbar .sidebar-toggle:hover {
background-color:
#3e355a;
}
.main-footer {
background-color: #1f2234;
padding: 15px;
color: #fff;
border-top: 0px;
}
.skin-blue .main-header .navbar {
background-color: #1f2234;
}
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
background-color:
#1f2234 !important;
}
.alert-danger, .alert-error {
border-color: #fff;
border: 1px solid
#fff;
border-radius: 7px;
}
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
color:
#f6f6f6;
background-color: #3e355a;
}
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
background-color:
#3e355a !important;
}
.box {
position: relative;
border-radius: 3px;
background-color: #333646;
border-top: 3px solid #8964a9;
margin-bottom: 20px;
width: 100%;
}
.table-striped > tbody > tr:nth-of-type(2n+1) {
background-color:
#333646;
}
.box-header {
color: #fff;
}
.box.box-danger {
border-top-color: #8964a9;
}
.box.box-primary {
border-top-color: #8964a9;
}
a {
color: #8964a9;
}
.box-header.with-border {
border-bottom: none;
}
a:hover, a:active, a:focus {
outline: none;
text-decoration: none;
color: #fff;
}
// .modal.in .modal-dialog{
// color:#000;
// }
.form-control {
background-color:#333646;
color: #fff;
}
.box-footer {
border-top: none;
background-color:
#333646;
}
.modal-footer {
border-top: none;
}
.modal-content {
background-color:
#333646;
}
.modal.in .modal-dialog {
background-color: #333646;
}
.layout-boxed {
background: none;
background-color: rgba(0, 0, 0, 0);
background-color:
#3e355a;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
background: none;
}
</style>
</head>
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
<div class="wrapper">
<header class="main-header">
<!-- Logo -->
<a href="{{ url_for('core.home') }}" class="logo"><b>LN</b>bits</a>
<!-- Header Navbar: style can be found in header.less -->
<nav class="navbar navbar-static-top" role="navigation">
<!-- Sidebar toggle button-->
<a
href="#"
class="sidebar-toggle"
data-toggle="offcanvas"
role="button"
>
<span class="sr-only">Toggle navigation</span>
</a>
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
<!-- Messages: style can be found in dropdown.less-->
<li class="dropdown messages-menu">
{% block messages %}{% endblock %}
</li>
</ul>
<p>{% raw %}{{amountWords}}{% endraw %}</p>
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
:disable="formDialog.data.name == '' || formDialog.data.email == '' || paymentReq"
type="submit"
>Submit</q-btn
>
<q-btn @click="resetForm" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</nav>
</header>
</q-form>
</q-card-section>
</q-card>
<aside class="main-sidebar">
<!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar" style="height: auto;">
<q-card v-show="ticketLink.show" class="q-pa-lg">
<div class="text-center q-mb-lg">
<a :href="ticketLink.data.link" target="_blank">
<q-btn unelevated size="xl" color="deep-purple"
>Link to your ticket!</q-btn
></a
>
<br /><br />
<p>You'll be redirected in a few moments...</p>
</div>
</q-card>
</div>
</section>
<!-- /.sidebar -->
</aside>
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
<q-card
v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="paymentReq"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div>
<!-- Right side column. Contains the navbar and content of the page -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
LNBits Events
<small>Lightning powered tickets</small>
</h1>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
console.log('{{ form_costpword }}')
Vue.component(VueQrcode.name, VueQrcode)
</section>
<!-- Main content -->
<section class="content"><br/><br/>
<center><h1 style="font-size:500%">{{ nme }}</h1></center>
<center><h2 style="width:55%;word-wrap: break-word;" >{{ descr }}</h2></center>
<div id="theform">
<br/><br/><br/>
<center>
<form role="form">
<div class="form-group" style="width:300px;">
<input id="Nam" type="text" class="form-control" placeholder="Name"></input>
<input id="Ema" type="text" class="form-control" placeholder="Email"></input>
</div>
<button onclick="submitforticket()" type="button" class="btn btn-info">Go to payment</button><p style="color:red;" id="error"></p>
</form>
</div>
</center>
<center><br/><br/> <div id="qrcode" style="width: 340px;"></div><br/><br/>
<div style="width:55%;word-wrap: break-word;" id="qrcodetxt"></div> <br/></center>
</section><!-- /.content -->
</div><!-- /.content-wrapper -->
</div>
</body>
<script>
function postAjax(url, data, thekey, success) {
var params =
typeof data == 'string'
? data
: Object.keys(data)
.map(function(k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k])
})
.join('&')
var xhr = window.XMLHttpRequest
? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP')
xhr.open('POST', url)
xhr.onreadystatechange = function() {
if (xhr.readyState > 3 && xhr.status == 200) {
success(xhr.responseText)
}
}
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(params)
return xhr
}
function getAjax(url, thekey, success) {
var xhr = window.XMLHttpRequest
? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP')
xhr.open('GET', url, true)
xhr.onreadystatechange = function() {
if (xhr.readyState > 3 && xhr.status == 200) {
success(xhr.responseText)
}
}
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send()
return xhr
}
function submitforticket(){
nam = document.getElementById('Nam').value
ema = document.getElementById('Ema').value
postAjax(
"{{ url_for('events.api_getticket') }}?ema=" + ema,
JSON.stringify({"unireg": "{{wave }}", "name": nam}),
"filla",
function(data) {
theinvoice = JSON.parse(data).pay_req
thehash = JSON.parse(data).payment_hash
new QRCode(document.getElementById('qrcode'), {
text: theinvoice,
width: 300,
height: 300,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
})
document.getElementById('theform').innerHTML = ""
document.getElementById("qrcode").style.backgroundColor = "white";
document.getElementById("qrcode").style.padding = "20px";
document.getElementById('qrcodetxt').innerHTML = theinvoice + "<br/><br/>"
var refreshId = setInterval(function(){
getAjax('/api/v1/invoice/' + thehash, "{{wave}}", function(datab) {
console.log(JSON.parse(datab).PAID)
if (JSON.parse(datab).PAID == 'TRUE') {
location.replace("{{ url_for('events.ticket') }}?hash="+thehash + "&unireg={{wave}}")
clearInterval(refreshId)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
paymentReq: null,
redirectUrl: null,
formDialog: {
show: false,
data: {
name: '',
email: ''
}
},
ticketLink: {
show: false,
data: {
link: ''
}
},
receive: {
show: false,
status: 'pending',
paymentReq: null
}
})}, 3000);
}
},
methods: {
resetForm: function (e) {
e.preventDefault()
this.formDialog.data.name = ''
this.formDialog.data.email = ''
},
})
}
closeReceiveDialog: function () {
var checker = this.receive.paymentChecker
dismissMsg()
clearInterval(paymentChecker)
setTimeout(function () {}, 10000)
},
Invoice: function () {
var self = this
axios
.post(
'/events/api/v1/tickets/' + '{{ event_id }}/{{ event_price }}',
{
event: '{{ event_id }}',
event_name: '{{ event_name }}',
name: self.formDialog.data.name,
email: self.formDialog.data.email
}
)
.then(function (response) {
self.paymentReq = response.data.payment_request
self.paymentCheck = response.data.checking_id
</script>
</html>
dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.receive = {
show: true,
status: 'pending',
paymentReq: self.paymentReq
}
paymentChecker = setInterval(function () {
axios
.get('/events/api/v1/tickets/' + self.paymentCheck)
.then(function (res) {
if (res.data.paid) {
clearInterval(paymentChecker)
dismissMsg()
self.formDialog.data.name = ''
self.formDialog.data.email = ''
self.$q.notify({
type: 'positive',
message: 'Sent, thank you!',
icon: null
})
self.receive = {
show: false,
status: 'complete',
paymentReq: null
}
self.ticketLink = {
show: true,
data: {
link: '/events/ticket/' + res.data.ticket_id
}
}
setTimeout(function () {
window.location.href =
'/events/ticket/' + res.data.ticket_id
}, 5000)
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}, 2000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
}
})
</script>
{% endblock %}
@@ -0,0 +1,35 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">{{ event_name }} error</h3>
<br />
<q-icon
name="warning"
class="text-grey"
style="font-size: 20rem;"
></q-icon>
<h5 class="q-my-none">{{ event_error }}</h5>
<br />
</center>
</q-card-section>
</q-card>
</div>
{% endblock %} {% block scripts %}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {}
}
})
</script>
{% endblock %}
</div>
@@ -1,480 +1,509 @@
<!-- @format -->
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
>New Event</q-btn
>
</q-card-section>
</q-card>
{% extends "legacy.html" %} {% block messages %}
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-bell-o"></i>
<span class="label label-danger">!</span>
</a>
<ul class="dropdown-menu">
<li class="header"><b>Instant wallet, bookmark to save</b></li>
<li></li>
</ul>
{% endblock %}
{% block menuitems %}
<li class="treeview">
<a href="#">
<i class="fa fa-bitcoin"></i> <span>Wallets</span>
<i class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
{% for w in user_wallets %}
<li>
<a href="{{ url_for('wallet') }}?wal={{ w.id }}&usr={{ w.user }}"><i class="fa fa-bolt"></i> {{ w.name }}</a>
</li>
{% endfor %}
<li><a onclick="sidebarmake()">Add a wallet +</a></li>
<div id="sidebarmake"></div>
</ul>
</li>
<li class="active treeview">
<a href="#">
<i class="fa fa-th"></i> <span>Extensions</span>
<i class="fa fa-angle-left pull-right"></i>
</a>
<ul class="treeview-menu">
{% for extension in EXTENSIONS %}
{% if extension.code in user_ext %}
<li>
<a href="{{ url_for(extension.code + '.index') }}?usr={{ user }}"><i class="fa fa-plus"></i> {{ extension.name }}</a>
</li>
{% endif %}
{% endfor %}
<li>
<a href="{{ url_for('core.extensions') }}?usr={{ user }}">Manager</a></li>
</ul>
</li>
{% endblock %}
{% block body %}
<!-- Right side column. Contains the navbar and content of the page -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
Events
<small>bitcoin tickets</small>
</h1>
<ol class="breadcrumb">
<li>
<a href="{{ url_for('wallet') }}?usr={{ user }}"><i class="fa fa-dashboard"></i> Home</a>
</li>
<li>
<a href="{{ url_for('core.extensions') }}?usr={{ user }}"><li class="fa fa-dashboard">Extensions</li></a>
</li>
<li>
<i class="active" class="fa fa-dashboard">Lightning tickets</i>
</li>
</ol>
<br /><br />
</section>
<style>
.datepicker-days{
background-color: #1f2234;
}
</style>
<!-- Main content -->
<section class="content">
<!-- Small boxes (Stat box) -->
<div class="row">
<div class="col-md-6">
<!-- general form elements -->
<div class="box box-primary">
<div class="box-header">
<h3 class="box-title"> Make a ticket wave</h3>
</div><!-- /.box-header -->
<!-- form start -->
<form role="form">
<div class="box-body">
<div class="form-group">
<label for="exampleInputEmail1">Ticket title</label>
<input id="tit" type="text" pattern="^[A-Za-z]+$" class="form-control" >
</div>
<div class="form-group">
<label>Description of event</label>
<textarea id="descr" class="form-control" rows="2"></textarea>
</div>
<!-- select -->
<div class="form-group">
<label>Select a wallet</label>
<select id="wal" class="form-control">
<option></option>
{% for w in user_wallets %}
<option>{{w.name}}-{{w.id}}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="nooftickets">No. of tickets</label>
<input id="notickets" type="number" class="form-control" placeholder="10" max="86400"></input>
</div>
<div class="form-group">
<label>Close date:</label>
<div class="form-group">
<input type="text" class="form-control" id="datepicker"></input>
</div>
<div class="form-group">
<label for="prpertick">Price per ticket</label>
<input id="prtickets" type="number" class="form-control" placeholder="10"></input>
</div>
</div><!-- /.box-body -->
<div class="box-footer">
<button onclick="postev()" type="button" class="btn btn-info">Create Wave</button><p style="color:red;" id="error"></p>
</div>
</form>
</div></div></div>
<div class="col-md-6">
<!-- general form elements -->
<div class="box box-primary">
<div class="box-header">
<h3 class="box-title">Select a link</h3>
</div><!-- /.box-header -->
<form role="form">
<div class="box-body">
<div class="form-group">
<select class="form-control" id="waveselect" onchange="drawwithdraw()">
<option value="none" selected>
Select an Option
</option>
{% for w in user_ev %}
<option id="{{w.uni}}" value="{{w.tit}}-{{w.unireg}}-{{w.uni}}">{{w.tit}}-{{w.unireg}}-{{w.uni}}</option>
{% endfor %}
</select>
</div>
<center> <br/><div id="qrcode" style="width:340px" ></div><br/><div style="width:75%;word-wrap: break-word;" id="qrcodetxt" ></div></center>
</div>
</form>
</div><!-- /.box -->
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="box">
<div class="box-header">
<h3 class="box-title">Ticket Waves<b id="withdraws"></b></h3>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Events</h5>
</div>
<!-- /.box-header -->
<div class="box-body no-padding">
<table id="pagnation" class="table table-bswearing anchorordered table-striped">
<tr>
<th>Title</th>
<th style="width:15%">Amt</th>
<th style="width:15%">Sold</th>
<th style="width:15%">Closing</th>
<th style="width:15%">Price</th>
<th style="width:15%">Wallet</th>
<th style="width:10%">Edit</th>
<th style="width:10%">Del</th>
</tr>
<tbody id="ticketwaves"></tbody>
</table>
<div class="col-auto">
<q-btn flat color="grey" @click="exporteventsCSV"
>Export to CSV</q-btn
>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
</div>
<q-table
dense
flat
:data="events"
row-key="id"
:columns="eventsTable.columns"
:pagination.sync="eventsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="link"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.displayUrl"
target="_blank"
></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="how_to_reg"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'/events/register/' + props.row.id"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="updateformDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteEvent(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<div id="editlink"></div>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Tickets</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportticketsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="tickets"
row-key="id"
:columns="ticketsTable.columns"
:pagination.sync="ticketsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="local_activity"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'/events/ticket/' + props.row.id"
target="_blank"
></q-btn>
</q-td>
<!-- /.content -->
</section>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<script>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteTicket(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits Events extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "events/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
//Date picker
$('#datepicker').datepicker({
autoclose: true
})
window.user = {{ user | tojson | safe }}
window.user_wallets = {{ user_wallets | tojson | safe }}
window.user_ext = {{ user_ext | tojson | safe }}
window.user_ev = {{ user_ev | tojson | safe }}
const user_ev = window.user_ev
console.log(user_ev)
function drawChart(user_ev) {
var transactionsHTML = ''
for (var i = 0; i < user_ev.length; i++) {
var ev = user_ev[i]
transactionsHTML =
"<tr><td style='width: 50%'>" +
ev.tit +
'</td><td>' +
ev.notickets +
'</td><td>' +
ev.sold +
'</td><td>' +
ev.cldate +
'</td><td>' +
ev.prtick +
'</td><td>' +
"<a href='{{ url_for('wallet') }}?usr="+ ev.usr +"'>" + ev.wal.substring(0, 4) + "...</a>" +
'</td><td>' +
"<i onclick='editlink("+ i +")'' class='fa fa-edit'></i>" +
'</td><td>' +
"<b><a style='color:red;' href='" + "{{ url_for('events.index') }}?del=" + ev.uni + "&usr=" + ev.usr +"'>" + "<i class='fa fa-trash'></i>" + "</a></b>" +
'</td></tr>' +
transactionsHTML
document.getElementById('ticketwaves').innerHTML = transactionsHTML
}
}
if (user_ev.length) {
drawChart(user_ev)
}
function postev(){
wal = document.getElementById('wal').value
tit = document.getElementById('tit').value
cldate = document.getElementById('datepicker').value
notickets = document.getElementById('notickets').value
prtickets = document.getElementById('prtickets').value
descr = document.getElementById('descr').value
if (tit == "") {
document.getElementById("error").innerHTML = "Only use letters in title"
return amt
}
if (wal == "") {
document.getElementById("error").innerHTML = "No wallet selected"
return amt
}
if (cldate == "") {
document.getElementById("error").innerHTML = "No date selected"
return amt
}
if (isNaN(notickets) || notickets < 1) {
document.getElementById("error").innerHTML = "Must be more than 1"
return amt
}
if (isNaN(prtickets) || prtickets < 10) {
document.getElementById("error").innerHTML = "Must be higher 10"
return amt
}
postAjax(
"{{ url_for('events.create') }}",
JSON.stringify({"tit": tit, "usr": user, "wal": wal, "notickets": notickets,"cldate": cldate, "prtickets": prtickets, "descr": descr}),
"filla",
function(data) { location.replace("{{ url_for('events.index') }}?usr=" + user)
})
}
function editlink(evnum){
evdetails = user_ev[evnum]
console.log(evdetails.descr)
wallpick = ""
checkbox = ""
if (evdetails.uniq == 1){
checkbox = "checked"}
document.getElementById('editlink').innerHTML = "<div class='row'>"+
"<div class='col-md-6'>"+
" <!-- general form elements -->"+
"<div class='box box-primary' style='min-height: 300px;'>"+
"<div class='box-header'>"+
"<h3 class='box-title'> Edit: <i id='unid'>" + evdetails.tit + "-" + evdetails.uni + "-" + evdetails.unireg + "</i> </h3>"+
"<div class='box-tools pull-right'>" +
"<button class='btn btn-box-tool' data-widget='remove'><i class='fa fa-times'></i></button>" +
"</div>" +
" </div><!-- /.box-header -->"+
" <!-- form start -->"+
"<form role='form'>"+
"<div class='box-body'>"+
"<div class='col-sm-3 col-md-4'>"+
"<div class='form-group'>"+
"<label for='exampleInputEmail1'>Link title</label>"+
"<input id='edittit' type='text' class='form-control' value='"+
evdetails.tit +
"'></input> </div>"+
" </div>"+
"<div class='col-sm-1 col-md-8'>"+
"<div class='form-group'>"+
"<label for='exampleInputEmail1'>Description of event</label>"+
"<textarea id='editdescr' type='textarea' rows='1' class='form-control'>"+evdetails.descr+"</textarea> </div>"+
" </div>"+
" <div class='col-sm-4 col-md-4'>"+
" <!-- select -->"+
" <div class='form-group'>"+
" <label>Select a wallet</label>"+
"<select id='editwal' class='form-control'>"+
" <option>" + evdetails.walnme + "-" + evdetails.wal + "</option>"+
" {% for w in user_wallets %}"+
" <option>{{w.name}}-{{w.id}}</option>"+
" {% endfor %}"+
" </select>"+
" </div>"+
" </div>"+
" <div class='col-sm-3 col-md-4'>"+
"<div class='form-group'>"+
" <label for='exampleInputPassword1'>No of tickets:</label>"+
" <input id='editnooftickets' type='number' class='form-control' placeholder='0' max='86400' value='"+
evdetails.notickets +
"'></input>"+
"</div> </div>"+
" <div class='col-sm-3 col-md-4'>"+
"<div class='form-group'>"+
"<label for='exampleInputEmail1'>Price per ticket:</label>"+
" <input id='editprtick' type='number' class='form-control' placeholder='1' value='"+
evdetails.prtick +
"'></input>"+
" </div></div>"+
" <div class='col-sm-3 col-md-4'>"+
" <div class='input-group date'>"+
" <label for='exampleInputEmail1'>Close date:</label>"+
" <input id='datepicker2' type='text' class='form-control' placeholder='1' value='"+
evdetails.cldate +
"'></input>"+
" </div></div>"+
" <div class='col-sm-3 col-md-4'>"+
"</div><!-- /.box-body -->"+
" </div><br/>"+
" <div class='box-footer'>"+
" <div class='col-sm-3 col-md-4'>"+
"<button onclick='editlinkcont()' type='button' style='margin: 24px;' class='btn btn-info'>Edit link(s)</button>"+
"</div>"+
"<p style='color:red;' id='error2'>.</p>"+
" </div></form></div><!-- /.box --></div></div>"
}
//Date picker
$('#datepicker2').datepicker({
autoclose: true
})
function editlinkcont(){
unid = document.getElementById('unid').innerHTML
wal = document.getElementById('editwal').value
tit = document.getElementById('edittit').value
nooftickets = document.getElementById('editnooftickets').value
prtick = document.getElementById('editprtick').value
cldate = document.getElementById('datepicker2').value
descr = document.getElementById('editdescr').value
uni = unid.split("-")[1]
if (tit == "") {
document.getElementById("error2").innerHTML = "Only use letters in title"
return amt
}
if (wal == "") {
document.getElementById("error2").innerHTML = "No wallet selected"
return amt
}
if (isNaN(nooftickets) || nooftickets < 1 || nooftickets > 1000000) {
document.getElementById("error2").innerHTML = "No. of tickets must be between 1 - 1000000"
return amt
}
if (isNaN(prtick) || prtick < 10 ) {
document.getElementById("error2").innerHTML = "Ticket pricket must be higher than 10"
return amt
}
postAjax(
"{{ url_for('events.create') }}",
JSON.stringify({"tit": tit, "usr": user, "wal": wal, "notickets": nooftickets,"cldate": cldate, "prtickets": prtick, "id": unid, "descr": descr}),
"filla",
function(data) { location.replace("{{ url_for('events.index') }}?usr=" + user)
})
}
//draws withdraw QR code
function drawwithdraw() {
document.getElementById("qrcode").innerHTML = "";
walname = document.getElementById("waveselect").value
thewave = walname.split("-");
console.log(window.location.hostname + "-" + thewave[1])
toencode = "/events/wave/" + thewave[1]
toreg = "/events/registration/" + thewave[2]
new QRCode(document.getElementById('qrcode'), {
text: toencode,
width: 300,
height: 300,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
})
document.getElementById('qrcodetxt').innerHTML = "<a href='" + toencode + "'><h2>Payment link</a> , " +
"<a href='" + toreg + "'>Registration</h2></a>"
document.getElementById("qrcode").style.backgroundColor = "white";
document.getElementById("qrcode").style.padding = "20px";
}
</script>
<q-dialog v-model="formDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendEventData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
v-model.trim="formDialog.data.name"
type="name"
label="Name of event "
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.info"
type="textarea"
label="Info about the event "
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.closing_date"
type="date"
label="Closing for tickets "
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.event_start_date"
type="date"
label="Event begins "
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.event_end_date"
type="date"
label="Event ends "
></q-input>
<q-input
filled
dense
v-model.number="formDialog.data.amount_tickets"
type="number"
label="Amount of tickets "
></q-input>
<q-input
filled
dense
v-model.number="formDialog.data.price_per_ticket"
type="number"
label="Price per ticket "
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="formDialog.data.id"
unelevated
color="deep-purple"
type="submit"
>Update Event</q-btn
>
<q-btn
v-else
unelevated
color="deep-purple"
:disable="formDialog.data.wallet == null || formDialog.data.name == null || formDialog.data.info == null || formDialog.data.closing_date == null || formDialog.data.event_start_date == null || formDialog.data.event_end_date == null || formDialog.data.amount_tickets == null || formDialog.data.price_per_ticket == null"
type="submit"
>Create Event</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapEvents = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.displayUrl = ['/events/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
events: [],
tickets: [],
eventsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'info', align: 'left', label: 'Info', field: 'info'},
{
name: 'event_start_date',
align: 'left',
label: 'Start date',
field: 'event_start_date'
},
{
name: 'event_end_date',
align: 'left',
label: 'End date',
field: 'event_end_date'
},
{
name: 'closing_date',
align: 'left',
label: 'Ticket close',
field: 'closing_date'
},
{
name: 'price_per_ticket',
align: 'left',
label: 'Price',
field: 'price_per_ticket'
},
{
name: 'amount_tickets',
align: 'left',
label: 'No tickets',
field: 'amount_tickets'
},
{
name: 'sold',
align: 'left',
label: 'Sold',
field: 'sold'
}
],
pagination: {
rowsPerPage: 10
}
},
ticketsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'event', align: 'left', label: 'Event', field: 'event'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'email', align: 'left', label: 'Email', field: 'email'},
{
name: 'registered',
align: 'left',
label: 'Registered',
field: 'registered'
}
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {}
}
}
},
methods: {
getTickets: function () {
var self = this
console.log('obj')
LNbits.api
.request(
'GET',
'/events/api/v1/tickets?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.tickets = response.data.map(function (obj) {
console.log(obj)
return mapEvents(obj)
})
})
},
deleteTicket: function (ticketId) {
var self = this
var tickets = _.findWhere(this.tickets, {id: ticketId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this ticket')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/events/api/v1/tickets/' + ticketId,
_.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey
)
.then(function (response) {
self.tickets = _.reject(self.tickets, function (obj) {
return obj.id == ticketId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportticketsCSV: function () {
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
},
getEvents: function () {
var self = this
LNbits.api
.request(
'GET',
'/events/api/v1/events?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.events = response.data.map(function (obj) {
return mapEvents(obj)
})
})
},
sendEventData: function () {
var wallet = _.findWhere(this.g.user.wallets, {
id: this.formDialog.data.wallet
})
var data = this.formDialog.data
if (data.id) {
this.updateEvent(wallet, data)
} else {
this.createEvent(wallet, data)
}
},
createEvent: function (wallet, data) {
var self = this
LNbits.api
.request('POST', '/events/api/v1/events', wallet.inkey, data)
.then(function (response) {
self.events.push(mapEvents(response.data))
self.formDialog.show = false
self.formDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
updateformDialog: function (formId) {
var link = _.findWhere(this.events, {id: formId})
console.log(link.id)
this.formDialog.data.id = link.id
this.formDialog.data.wallet = link.wallet
this.formDialog.data.name = link.name
this.formDialog.data.info = link.info
this.formDialog.data.closing_date = link.closing_date
this.formDialog.data.event_start_date = link.event_start_date
this.formDialog.data.event_end_date = link.event_end_date
this.formDialog.data.amount_tickets = link.amount_tickets
this.formDialog.data.price_per_ticket = link.price_per_ticket
this.formDialog.show = true
},
updateEvent: function (wallet, data) {
var self = this
console.log(data)
LNbits.api
.request(
'PUT',
'/events/api/v1/events/' + data.id,
wallet.inkey,
data
)
.then(function (response) {
self.events = _.reject(self.events, function (obj) {
return obj.id == data.id
})
self.events.push(mapEvents(response.data))
self.formDialog.show = false
self.formDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteEvent: function (eventsId) {
var self = this
var events = _.findWhere(this.events, {id: eventsId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this form link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/events/api/v1/events/' + eventsId,
_.findWhere(self.g.user.wallets, {id: events.wallet}).inkey
)
.then(function (response) {
self.events = _.reject(self.events, function (obj) {
return obj.id == eventsId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exporteventsCSV: function () {
LNbits.utils.exportCSV(this.eventsTable.columns, this.events)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getTickets()
this.getEvents()
}
}
})
</script>
{% endblock %}
@@ -0,0 +1,190 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">{{ event_name }} Registration</h3>
<br />
<br />
<q-btn unelevated color="deep-purple" @click="showCamera" size="xl"
>Scan ticket</q-btn
>
</center>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<q-table
dense
flat
:data="tickets"
row-key="id"
:columns="ticketsTable.columns"
:pagination.sync="ticketsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="local_activity"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'/events/ticket/' + props.row.id"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="sendCamera.show" position="top">
<q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg">
<qrcode-stream
@decode="decodeQR"
class="rounded-borders"
></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block styles %}
<link
rel="stylesheet"
type="text/css"
href="{{ url_for('static', filename='vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.css') }}"
/>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
{% assets filters='rjsmin', output='__bundle__/core/chart.js',
'vendor/moment@2.25.1/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% assets filters='rjsmin', output='__bundle__/core/wallet.js',
'vendor/bolt11/utils.js', 'vendor/bolt11/decoder.js',
'vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
Vue.use(VueQrcodeReader)
var mapEvents = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.displayUrl = ['/events/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tickets: [],
ticketsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{
name: 'registered',
align: 'left',
label: 'Registered',
field: 'registered'
}
],
pagination: {
rowsPerPage: 10
}
},
sendCamera: {
show: false,
camera: 'auto'
}
}
},
methods: {
hoverEmail: function (tmp) {
this.tickets.data.emailtemp = tmp
},
closeCamera: function () {
this.sendCamera.show = false
},
showCamera: function () {
this.sendCamera.show = true
},
decodeQR: function (res) {
this.sendCamera.show = false
var self = this
LNbits.api
.request('GET', '/events/api/v1/register/ticket/' + res)
.then(function (response) {
self.$q.notify({
type: 'positive',
message: 'Registered!'
})
setTimeout(function () {
window.location.reload()
}, 2000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getEventTickets: function () {
var self = this
console.log('obj')
LNbits.api
.request(
'GET',
'/events/api/v1/eventtickets/{{ wallet_id }}/{{ event_id }}'
)
.then(function (response) {
self.tickets = response.data.map(function (obj) {
return mapEvents(obj)
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
},
created: function () {
this.getEventTickets()
}
})
</script>
{% endblock %}
@@ -1,690 +0,0 @@
<!-- @format -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>LNBits Wallet</title>
<meta
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
name="viewport"
/>
<!-- Bootstrap 3.3.2 -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
/>
<!-- FontAwesome 4.3.0 -->
<link
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
rel="stylesheet"
type="text/css"
/>
<!-- Ionicons 2.0.0 -->
<link
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
rel="stylesheet"
type="text/css"
/>
<!-- Theme style -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
/>
<!-- AdminLTE Skins. Choose a skin from the css/skins
folder instead of downloading all of them to reduce the load. -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
/>
<!-- Morris chart -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
/>
<!-- jvectormap -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
/>
<!-- bootstrap wysihtml5 - text editor -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
/>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
<style>
.small-box > .small-box-footer {
text-align: left;
padding-left: 10px;
}
#loadingMessage {
text-align: center;
padding: 40px;
background-color: #eee;
}
#canvas {
width: 100%;
}
#output {
margin-top: 20px;
background: #eee;
padding: 10px;
padding-bottom: 0;
}
#output div {
padding-bottom: 10px;
word-wrap: break-word;
}
#noQRFound {
text-align: center;
}
</style>
<!-- jQuery 2.1.3 -->
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
<!-- jQuery UI 1.11.2 -->
<script
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
type="text/javascript"
></script>
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
<script>
$.widget.bridge('uibutton', $.ui.button)
</script>
<!-- Bootstrap 3.3.2 JS -->
<script
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
type="text/javascript"
></script>
<!-- Morris.js charts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
type="text/javascript"
></script>
<!-- Sparkline -->
<script
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
type="text/javascript"
></script>
<!-- jvectormap -->
<script
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
type="text/javascript"
></script>
<!-- jQuery Knob Chart -->
<script
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
type="text/javascript"
></script>
<!-- Bootstrap WYSIHTML5 -->
<script
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
type="text/javascript"
></script>
<!-- Slimscroll -->
<script
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
type="text/javascript"
></script>
<!-- FastClick -->
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
<!-- AdminLTE App -->
<script
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
type="text/javascript"
></script>
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
<script
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
type="text/javascript"
></script>
<!-- AdminLTE for demo purposes -->
<script
src="{{ url_for('static', filename='dist/js/demo.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
type="text/javascript"
></script>
<link
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
<script
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
type="text/javascript"
></script>
<style>
//GOOFY CSS HACK TO GO DARK
.skin-blue .wrapper {
background:
#1f2234;
}
body {
color: #fff;
}
.skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#1f2234;
border-left-color:#8964a9;
}
.skin-blue .main-header .navbar {
background-color:
#2e507d;
}
.content-wrapper, .right-side {
background-color:
#1f2234;
}
.skin-blue .main-header .logo {
background-color:
#1f2234;
color:
#fff;
}
.skin-blue .sidebar-menu > li.header {
color:
#4b646f;
background:
#1f2234;
}
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > .treeview-menu {
margin: 0 1px;
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > a {
border-left: 3px solid
transparent;
margin-right: 1px;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#3e355a;
border-left-color:#8964a9;
}
.skin-blue .main-header .logo:hover {
background:
#3e355a;
}
.skin-blue .main-header .navbar .sidebar-toggle:hover {
background-color:
#3e355a;
}
.main-footer {
background-color: #1f2234;
padding: 15px;
color: #fff;
border-top: 0px;
}
.skin-blue .main-header .navbar {
background-color: #1f2234;
}
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
background-color:
#1f2234 !important;
}
.alert-danger, .alert-error {
border-color: #fff;
border: 1px solid
#fff;
border-radius: 7px;
}
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
color:
#f6f6f6;
background-color: #3e355a;
}
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
background-color:
#3e355a !important;
}
.box {
position: relative;
border-radius: 3px;
background-color: #333646;
border-top: 3px solid #8964a9;
margin-bottom: 20px;
width: 100%;
}
.table-striped > tbody > tr:nth-of-type(2n+1) {
background-color:
#333646;
}
.box-header {
color: #fff;
}
.box.box-danger {
border-top-color: #8964a9;
}
.box.box-primary {
border-top-color: #8964a9;
}
a {
color: #8964a9;
}
.box-header.with-border {
border-bottom: none;
}
a:hover, a:active, a:focus {
outline: none;
text-decoration: none;
color: #fff;
}
// .modal.in .modal-dialog{
// color:#000;
// }
.form-control {
background-color:#333646;
color: #fff;
}
.box-footer {
border-top: none;
background-color:
#333646;
}
.modal-footer {
border-top: none;
}
.modal-content {
background-color:
#333646;
}
.modal.in .modal-dialog {
background-color: #333646;
}
.layout-boxed {
background: none;
background-color: rgba(0, 0, 0, 0);
background-color:
#3e355a;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
background: none;
}
</style>
</head>
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
<div class="wrapper">
<header class="main-header">
<!-- Logo -->
<a href="{{ url_for('core.home') }}" class="logo"><b>LN</b>bits</a>
<!-- Header Navbar: style can be found in header.less -->
<nav class="navbar navbar-static-top" role="navigation">
<!-- Sidebar toggle button-->
<a
href="#"
class="sidebar-toggle"
data-toggle="offcanvas"
role="button"
>
<span class="sr-only">Toggle navigation</span>
</a>
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
<!-- Messages: style can be found in dropdown.less-->
<li class="dropdown messages-menu">
{% block messages %}{% endblock %}
</li>
</ul>
</div>
</nav>
</header>
<aside class="main-sidebar">
<!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar" style="height: auto;">
<!-- Sidebar user panel -->
</section>
<!-- /.sidebar -->
</aside>
<!-- Right side column. Contains the navbar and content of the page -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
LNBits Events
<small>Lightning powered tickets</small>
</h1>
</section>
<!-- Main content -->
<section class="content"><br/><br/>
<center><h1 style="font-size:500%">{{ user_ev[0][6] }}</h1></center>
<br/><br/><br/>
<div class='modal fade sends' tabindex='-1' role='dialog' aria-labelledby='myLargeModalLabel' aria-hidden='true'>
<div class='modal-dialog' >
<div id='scantickets' style='padding: 0 10px 0 10px;'>
</div></div></div>
<center>
<button
onclick="scanQRsend()"
class="btn btn-block btn-primary btn-lg"
data-toggle="modal"
data-target=".sends"
style="width:300px;"
>
Scan ticket
</button>
</center>
<div id="scantickets"></div>
<br/><br/><br/>
<div id="qrcodetxt"></div> <br/></center>
<br/><br/><br/>
<center>
<div class="row" style="width:80%;margin-top:80px">
<style>
.ema, button:focus .txt {
display: none;
}
button:focus .ema {
display: block;
}
</style>
<div class="box">
<div class="box-header">
<h3 class="box-title">Attendees<b id="withdraws"></b></h3>
</div>
<!-- /.box-header -->
<div class="box-body no-padding">
<table id="pagnation" class="table table-bswearing anchorordered table-striped">
<tr>
<th style="width:20%">Name</th>
<th style="width:20%">Email</th>
<th style="width:50%">Ticket</th>
<th style="width:10%">Registered</th>
</tr>
<tbody id="ticketwaves"></tbody>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
</center>
</section><!-- /.content -->
</div><!-- /.content-wrapper -->
</div>
</body>
<script>
window.user_ev = {{ user_ev | tojson | safe }}
window.user_ev_sold = {{ user_ev_sold | tojson | safe }}
console.log(user_ev)
console.log(user_ev_sold)
function drawChart(user_ev_sold) {
var transactionsHTML = ''
for (var i = 0; i < user_ev_sold.length; i++) {
var ev = user_ev_sold[i]
transactionsHTML =
"<tr><td>" +
ev.name +
'</td><td>' +
'<button style="background-color: #333646;padding: 0;border: none;background: none;" class="lost"><span class="txt">xxxxxx</span>' +
'<span class="ema">' + ev.email + '</span></button>'+
'</td><td>' +
ev.hash +
'</td><td>' +
ev.reg +
'</td></tr>' +
transactionsHTML
document.getElementById('ticketwaves').innerHTML = transactionsHTML
}
}
if (user_ev_sold.length) {
drawChart(user_ev_sold)
}
function postAjax(url, data, thekey, success) {
var params =
typeof data == 'string'
? data
: Object.keys(data)
.map(function(k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k])
})
.join('&')
var xhr = window.XMLHttpRequest
? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP')
xhr.open('POST', url)
xhr.onreadystatechange = function() {
if (xhr.readyState > 3 && xhr.status == 200) {
success(xhr.responseText)
}
}
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(params)
return xhr
}
function getAjax(url, thekey, success) {
var xhr = window.XMLHttpRequest
? new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP')
xhr.open('GET', url, true)
xhr.onreadystatechange = function() {
if (xhr.readyState > 3 && xhr.status == 200) {
success(xhr.responseText)
}
}
xhr.setRequestHeader('X-Api-Key', thekey)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send()
return xhr
}
function scanQRsend() {
document.getElementById('scantickets').innerHTML =
"<div class='modal-content'>"+
"<br/><div id='registered'><div id='loadingMessage'>🎥 Unable to access video stream (please make sure you have a webcam enabled)</div>" +
"<canvas id='canvas' hidden></canvas><div id='output' hidden><div id='outputMessage'></div>" +
"<br/><span id='outputData'></span></div></div><div class='modal-footer'>"+
"<button type='submit' class='btn btn-primary' onclick='cancelsend()'>Cancel</button><br/><br/></div>"
var video = document.createElement('video')
var canvasElement = document.getElementById('canvas')
var canvas = canvasElement.getContext('2d')
var loadingMessage = document.getElementById('loadingMessage')
var outputContainer = document.getElementById('output')
var outputMessage = document.getElementById('outputMessage')
var outputData = document.getElementById('outputData')
// Use facingMode: environment to attemt to get the front camera on phones
navigator.mediaDevices
.getUserMedia({video: {facingMode: 'environment'}})
.then(function(stream) {
video.srcObject = stream
video.setAttribute('playsinline', true) // required to tell iOS safari we don't want fullscreen
video.play()
requestAnimationFrame(tick)
})
function tick() {
loadingMessage.innerText = '⌛ Loading video...'
if (video.readyState === video.HAVE_ENOUGH_DATA) {
loadingMessage.hidden = true
canvasElement.hidden = false
outputContainer.hidden = false
canvasElement.height = video.videoHeight
canvasElement.width = video.videoWidth
canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height)
var imageData = canvas.getImageData(
0,
0,
canvasElement.width,
canvasElement.height
)
var code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'dontInvert'
})
if (code) {
thehash = code.data
document.getElementById('registered').innerHTML = "<h1 style='color:green;font-size:300%;'>Registered!</h1>"
outputMessage.hidden = true
outputData.parentElement.hidden = false
outputData.innerText = JSON.stringify(code.data)
getAjax("{{ url_for('events.api_checkticket') }}?thehash=" + thehash, "filla", function(datab) {
if (JSON.parse(datab).status == 'TRUE') {
location.reload()
}
})
} else {
outputMessage.hidden = false
outputData.parentElement.hidden = true
}
}
requestAnimationFrame(tick)
}
}
function cancelsend() {
location.reload();
}
function getUrlVars() {
var vars = {};
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
vars[key] = value;
});
return vars;
}
var name = getUrlVars()["name"];
var thehash = getUrlVars()["thehash"];
console.log(thehash)
if(thehash != null){
document.getElementById('qrcodetxt').innerHTML = "<center><h1>" + name + " is registered!</h1></center>"
}
</script>
</html>
@@ -1,474 +1,46 @@
<!-- @format -->
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">{{ ticket_name }} Ticket</h3>
<br />
<h5 class="q-my-none">
Bookmark, print or screenshot this page,<br />
and present it for registration!
</h5>
<br />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>LNBits Wallet</title>
<meta
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
name="viewport"
/>
<!-- Bootstrap 3.3.2 -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
/>
<!-- FontAwesome 4.3.0 -->
<link
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
rel="stylesheet"
type="text/css"
/>
<!-- Ionicons 2.0.0 -->
<link
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
rel="stylesheet"
type="text/css"
/>
<!-- Theme style -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
/>
<!-- AdminLTE Skins. Choose a skin from the css/skins
folder instead of downloading all of them to reduce the load. -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
/>
<!-- Morris chart -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
/>
<!-- jvectormap -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
/>
<!-- bootstrap wysihtml5 - text editor -->
<link
rel="stylesheet"
media="screen"
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
/>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
<style>
.small-box > .small-box-footer {
text-align: left;
padding-left: 10px;
}
#loadingMessage {
text-align: center;
padding: 40px;
background-color: #eee;
}
#canvas {
width: 100%;
}
#output {
margin-top: 20px;
background: #eee;
padding: 10px;
padding-bottom: 0;
}
#output div {
padding-bottom: 10px;
word-wrap: break-word;
}
#noQRFound {
text-align: center;
}
</style>
<!-- jQuery 2.1.3 -->
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
<!-- jQuery UI 1.11.2 -->
<script
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
type="text/javascript"
></script>
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
<script>
$.widget.bridge('uibutton', $.ui.button)
</script>
<!-- Bootstrap 3.3.2 JS -->
<script
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
type="text/javascript"
></script>
<!-- Morris.js charts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
type="text/javascript"
></script>
<!-- Sparkline -->
<script
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
type="text/javascript"
></script>
<!-- jvectormap -->
<script
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
type="text/javascript"
></script>
<!-- jQuery Knob Chart -->
<script
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
type="text/javascript"
></script>
<!-- Bootstrap WYSIHTML5 -->
<script
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
type="text/javascript"
></script>
<!-- Slimscroll -->
<script
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
type="text/javascript"
></script>
<!-- FastClick -->
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
<!-- AdminLTE App -->
<script
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
type="text/javascript"
></script>
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
<script
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
type="text/javascript"
></script>
<!-- AdminLTE for demo purposes -->
<script
src="{{ url_for('static', filename='dist/js/demo.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
type="text/javascript"
></script>
<link
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
<script
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
type="text/javascript"
></script>
<script
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
type="text/javascript"
></script>
<style>
//GOOFY CSS HACK TO GO DARK
.skin-blue .wrapper {
background:
#1f2234;
}
body {
color: #fff;
}
.skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#1f2234;
border-left-color:#8964a9;
}
.skin-blue .main-header .navbar {
background-color:
#2e507d;
}
.content-wrapper, .right-side {
background-color:
#1f2234;
}
.skin-blue .main-header .logo {
background-color:
#1f2234;
color:
#fff;
}
.skin-blue .sidebar-menu > li.header {
color:
#4b646f;
background:
#1f2234;
}
.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side {
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > .treeview-menu {
margin: 0 1px;
background:
#1f2234;
}
.skin-blue .sidebar-menu > li > a {
border-left: 3px solid
transparent;
margin-right: 1px;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
color: #fff;
background:#3e355a;
border-left-color:#8964a9;
}
.skin-blue .main-header .logo:hover {
background:
#3e355a;
}
.skin-blue .main-header .navbar .sidebar-toggle:hover {
background-color:
#3e355a;
}
.main-footer {
background-color: #1f2234;
padding: 15px;
color: #fff;
border-top: 0px;
}
.skin-blue .main-header .navbar {
background-color: #1f2234;
}
.bg-red, .callout.callout-danger, .alert-danger, .alert-error, .label-danger, .modal-danger .modal-body {
background-color:
#1f2234 !important;
}
.alert-danger, .alert-error {
border-color: #fff;
border: 1px solid
#fff;
border-radius: 7px;
}
.skin-blue .main-header .navbar .nav > li > a:hover, .skin-blue .main-header .navbar .nav > li > a:active, .skin-blue .main-header .navbar .nav > li > a:focus, .skin-blue .main-header .navbar .nav .open > a, .skin-blue .main-header .navbar .nav .open > a:hover, .skin-blue .main-header .navbar .nav .open > a:focus {
color:
#f6f6f6;
background-color: #3e355a;
}
.bg-aqua, .callout.callout-info, .alert-info, .label-info, .modal-info .modal-body {
background-color:
#3e355a !important;
}
.box {
position: relative;
border-radius: 3px;
background-color: #333646;
border-top: 3px solid #8964a9;
margin-bottom: 20px;
width: 100%;
}
.table-striped > tbody > tr:nth-of-type(2n+1) {
background-color:
#333646;
}
.box-header {
color: #fff;
}
.box.box-danger {
border-top-color: #8964a9;
}
.box.box-primary {
border-top-color: #8964a9;
}
a {
color: #8964a9;
}
.box-header.with-border {
border-bottom: none;
}
a:hover, a:active, a:focus {
outline: none;
text-decoration: none;
color: #fff;
}
// .modal.in .modal-dialog{
// color:#000;
// }
.form-control {
background-color:#333646;
color: #fff;
}
.box-footer {
border-top: none;
background-color:
#333646;
}
.modal-footer {
border-top: none;
}
.modal-content {
background-color:
#333646;
}
.modal.in .modal-dialog {
background-color: #333646;
}
.layout-boxed {
background: none;
background-color: rgba(0, 0, 0, 0);
background-color:
#3e355a;
}
.skin-blue .sidebar-menu > li > a:hover, .skin-blue .sidebar-menu > li.active > a {
background: none;
}
</style>
</head>
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
<div class="wrapper">
<header class="main-header">
<!-- Logo -->
<a href="{{ url_for('core.home') }}" class="logo"><b>LN</b>bits</a>
<!-- Header Navbar: style can be found in header.less -->
<nav class="navbar navbar-static-top" role="navigation">
<!-- Sidebar toggle button-->
<a
href="#"
class="sidebar-toggle"
data-toggle="offcanvas"
role="button"
<qrcode
:value="'{{ ticket_id }}'"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
<br />
<q-btn @click="printWindow" color="grey" class="q-ml-auto">
<q-icon left size="3em" name="print"></q-icon> Print</q-btn
>
<span class="sr-only">Toggle navigation</span>
</a>
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
<!-- Messages: style can be found in dropdown.less-->
<li class="dropdown messages-menu">
{% block messages %}{% endblock %}
</li>
</ul>
</div>
</nav>
</header>
<aside class="main-sidebar">
<!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar" style="height: auto;">
</section>
<!-- /.sidebar -->
</aside>
<!-- Right side column. Contains the navbar and content of the page -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
LNBits Events
<small>Lightning powered tickets</small>
</h1>
</section>
<!-- Main content -->
<section class="content"><br/><br/>
<center><h2 style="width:70%;font-size:400%" >Bookmark/Screenshot this page. <br/>It is your ticket!</h2></center>
<center> <div style="width:340px;background-color:white;padding:20px"id="qrcode"></div></center>
</section><!-- /.content -->
</div><!-- /.content-wrapper -->
</div>
</body>
</center>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
new QRCode(document.getElementById('qrcode'), {
text: "{{ticket}}",
width: 300,
height: 300,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
})
</script>
</html>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {}
},
methods: {
printWindow: function () {
window.print()
}
}
})
</script>
{% endblock %}
+26 -155
View File
@@ -1,174 +1,45 @@
import uuid
from flask import g, abort, render_template
from datetime import date, datetime
from flask import jsonify, render_template, request, redirect, url_for
from datetime import datetime
from lnbits.decorators import check_user_exists, validate_uuids
from http import HTTPStatus
from lnbits.db import open_db, open_ext_db
from lnbits.extensions.events import events_ext
from .crud import get_ticket, get_event
@events_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
def index():
"""Main events link page."""
usr = request.args.get("usr")
return render_template("events/index.html", user=g.user)
if usr:
if not len(usr) > 20:
return redirect(url_for("home"))
# Get all the data
with open_db() as db:
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,))
user_ext = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (usr,))
user_ext = [v[0] for v in user_ext]
@events_ext.route("/<event_id>")
def display(event_id):
event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
if event.amount_tickets < 1:
return render_template("events/error.html", 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 render_template("events/error.html", event_name=event.name, event_error="Sorry, ticket closing date has passed :(")
with open_ext_db("events") as events_ext_db:
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE usr = ?", (usr,))
# If del is selected by user from events page, the event link is to be deleted
evdel = request.args.get("del")
if evdel:
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE uni = ?", (evdel,))
events_ext_db.execute("DELETE FROM events WHERE uni = ?", (evdel,))
if user_ev[0][9] > 0:
events_ext_db.execute("DELETE FROM eventssold WHERE uni = ?", (user_ev[0][12],))
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE usr = ?", (usr,))
print(user_ext)
return render_template("events/display.html", event_id=event_id, event_name=event.name, event_info=event.info, event_price=event.price_per_ticket)
return render_template(
"events/index.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_ev=user_ev
)
@events_ext.route("/create", methods=["GET", "POST"])
def create():
"""."""
@events_ext.route("/ticket/<ticket_id>")
def ticket(ticket_id):
ticket = get_ticket(ticket_id) or abort(HTTPStatus.NOT_FOUND, "Ticket does not exist.")
event = get_event(ticket.event) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
return render_template("events/ticket.html", ticket_id=ticket_id, ticket_name=event.name, ticket_info=event.info)
data = request.json
tit = data["tit"]
wal = data["wal"]
cldate = data["cldate"]
notickets = data["notickets"]
prtick = data["prtickets"]
usr = data["usr"]
descr = data["descr"]
wall = wal.split("-")
# Form validation
if (
not tit.replace(" ", "").isalnum()
or wal == ""
or int(notickets) < 0
or int(prtick) < 0
):
return jsonify({"ERROR": "FORM ERROR"}), 401
# If id that means its a link being edited, delete the record first
if "id" in data:
unid = data["id"].split("-")
uni = unid[1]
unireg = unid[2]
with open_ext_db("events") as events_ext_db:
events_ext_db.execute("DELETE FROM events WHERE uni = ?", (unid[1],))
else:
uni = uuid.uuid4().hex
unireg = uuid.uuid4().hex
@events_ext.route("/register/<event_id>")
def register(event_id):
event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
with open_db() as dbb:
user_wallets = dbb.fetchall("SELECT * FROM wallets WHERE user = ? AND id = ?", (usr, wall[1],))
if not user_wallets:
return jsonify({"ERROR": "NO WALLET USER"}), 401
with open_db() as db:
user_ext = db.fetchall("SELECT * FROM extensions WHERE user = ?", (usr,))
user_ext = [v[0] for v in user_ext]
# Add to DB
with open_ext_db("events") as events_ext_db:
events_ext_db.execute(
"""
INSERT OR IGNORE INTO events
(usr, wal, walnme, walinvkey, uni, tit, cldate, notickets, prtick, descr, unireg)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
usr,
wall[1],
user_wallets[0][1],
user_wallets[0][4],
uni,
tit,
cldate,
notickets,
prtick,
descr,
unireg,
),
)
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE usr = ?", (usr,))
if not user_ev:
return jsonify({"ERROR": "NO WALLET USER"}), 401
return render_template(
"events/index.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_ev=user_ev
)
return render_template("events/register.html", event_id=event_id, event_name=event.name, wallet_id=event.wallet)
@events_ext.route("/wave/<wave>/", methods=["GET", "POST"])
def wave(wave):
"""."""
with open_ext_db("events") as events_ext_db:
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE unireg = ?", (wave,))
if not user_ev:
return jsonify({"ERROR": "NO RECORD"}), 401
return render_template(
"events/display.html", wave=wave, nme=user_ev[0][6], descr=user_ev[0][11]
)
@events_ext.route("/registration/<wave>", methods=["GET", "POST"])
def registration(wave):
"""."""
with open_ext_db("events") as events_ext_db:
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE uni = ?", (wave,))
user_ev_sold = events_ext_db.fetchall("SELECT * FROM eventssold WHERE uni = ? AND paid = 1", (user_ev[0][12],))
if not user_ev:
return jsonify({"ERROR": "NO RECORD"}), 401
return render_template(
"events/registration.html", user_ev=user_ev, user_ev_sold=user_ev_sold
)
@events_ext.route("/ticket/", methods=["GET"])
def ticket():
"""."""
thehash = request.args.get("hash")
unireg = request.args.get("unireg")
#Double check the payment has cleared
with open_db() as db:
payment = db.fetchall("SELECT * FROM apipayments WHERE payhash = ?", (thehash,))
if not payment:
return jsonify({"status": "ERROR", "reason":"NO RECORD OF PAYMENT"}), 400
if payment[0][4] == 1:
return jsonify({"status": "ERROR", "reason":"NOT PAID"}), 400
#Update databases
with open_ext_db("events") as events_ext_db:
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE unireg = ?", (unireg,))
updatesold = user_ev[0][9] + 1
events_ext_db.execute("UPDATE events SET sold = ? WHERE unireg = ?", (updatesold, unireg,))
events_ext_db.execute("UPDATE eventssold SET paid = 1 WHERE hash = ?", (thehash,))
eventssold = events_ext_db.fetchall("SELECT * FROM eventssold WHERE hash = ?", (thehash,))
if not eventssold:
return jsonify({"status": "ERROR", "reason":"NO TICKET RECORD"}), 200
return render_template(
"events/ticket.html", name=eventssold[0][3], ticket=thehash
)
+158 -52
View File
@@ -1,65 +1,171 @@
import uuid
import json
import requests
from flask import g, jsonify, request
from http import HTTPStatus
from flask import jsonify, request, url_for
from datetime import datetime
from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.settings import WALLET
from lnbits.db import open_db, open_ext_db
from lnbits.extensions.events import events_ext
@events_ext.route("/api/v1/getticket/", methods=["GET","POST"])
def api_getticket():
"""."""
data = request.json
unireg = data["unireg"]
name = data["name"]
email = request.args.get("ema")
with open_ext_db("events") as events_ext_db:
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE unireg = ?", (unireg,))
from .crud import create_ticket, update_ticket, get_ticket, get_tickets, delete_ticket, create_event, update_event, get_event, get_events, delete_event, get_event_tickets, reg_ticket
header = {"Content-Type": "application/json", "X-Api-Key": user_ev[0][4]}
data = {"value": str(user_ev[0][10]), "memo": user_ev[0][6]}
print(url_for("api_invoices", _external=True))
r = requests.post(url=url_for("api_invoices", _external=True), headers=header, data=json.dumps(data))
r_json = r.json()
#########Events##########
if "ERROR" in r_json:
return jsonify({"status": "ERROR", "reason": r_json["ERROR"]}), 400
events_ext_db.execute(
"""
INSERT OR IGNORE INTO eventssold
(uni, email, name, hash)
VALUES (?, ?, ?, ?)
""",
(
unireg,
email,
name,
r_json["payment_hash"]
),
@events_ext.route("/api/v1/events", methods=["GET"])
@api_check_wallet_key("invoice")
def api_events():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([event._asdict() for event in get_events(wallet_ids)]), HTTPStatus.OK
@events_ext.route("/api/v1/events", methods=["POST"])
@events_ext.route("/api/v1/events/<event_id>", methods=["PUT"])
@api_check_wallet_key("invoice")
@api_validate_post_request(
schema={
"wallet": {"type": "string", "empty": False, "required": True},
"name": {"type": "string", "empty": False, "required": True},
"info": {"type": "string", "min": 0, "required": True},
"closing_date": {"type": "string", "empty": False, "required": True},
"event_start_date": {"type": "string", "empty": False, "required": True},
"event_end_date": {"type": "string", "empty": False, "required": True},
"amount_tickets": {"type": "integer", "min": 0, "required": True},
"price_per_ticket": {"type": "integer", "min": 0, "required": True}
}
)
def api_event_create(event_id=None):
if event_id:
event = get_event(event_id)
print(g.data)
if not event:
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
if event.wallet != g.wallet.id:
return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN
event = update_event(event_id, **g.data)
else:
event = create_event(**g.data)
print(event)
return jsonify(event._asdict()), HTTPStatus.CREATED
@events_ext.route("/api/v1/events/<event_id>", methods=["DELETE"])
@api_check_wallet_key("invoice")
def api_form_delete(event_id):
event = get_event(event_id)
if not event:
return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND
if event.wallet != g.wallet.id:
return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN
delete_event(event_id)
return "", HTTPStatus.NO_CONTENT
#########Tickets##########
@events_ext.route("/api/v1/tickets", methods=["GET"])
@api_check_wallet_key("invoice")
def api_tickets():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([ticket._asdict() for ticket in get_tickets(wallet_ids)]), HTTPStatus.OK
@events_ext.route("/api/v1/tickets/<event_id>/<sats>", methods=["POST"])
@api_validate_post_request(
schema={
"name": {"type": "string", "empty": False, "required": True},
"email": {"type": "string", "empty": False, "required": True}
})
def api_ticket_make_ticket(event_id, sats):
event = get_event(event_id)
if not event:
return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND
try:
checking_id, payment_request = create_invoice(
wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {event_id}"
)
except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return jsonify({"status": "TRUE", "pay_req": r_json["pay_req"], "payment_hash": r_json["payment_hash"]}), 200
ticket = create_ticket(checking_id=checking_id, wallet=event.wallet, event=event_id, **g.data)
if not ticket:
return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK
@events_ext.route("/api/v1/checkticket/", methods=["GET"])
def api_checkticket():
"""."""
thehash = request.args.get("thehash")
#Check databases
with open_ext_db("events") as events_ext_db:
eventssold = events_ext_db.fetchall("SELECT * FROM eventssold WHERE hash = ?", (thehash,))
if not eventssold:
return jsonify({"status": "ERROR", "reason":"NO TICKET RECORD"}), 200
if eventssold[0][4] == 0:
return jsonify({"status": "ERROR", "reason":"NOT PAID"}), 200
@events_ext.route("/api/v1/tickets/<checking_id>", methods=["GET"])
def api_ticket_send_ticket(checking_id):
theticket = get_ticket(checking_id)
try:
is_paid = not WALLET.get_invoice_status(checking_id).pending
except Exception:
return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND
with open_ext_db("events") as events_ext_db:
events_ext_db.execute("UPDATE eventssold SET reg = 1 WHERE hash = ?", (thehash,))
if is_paid:
wallet = get_wallet(theticket.wallet)
payment = wallet.get_payment(checking_id)
payment.set_pending(False)
ticket = update_ticket(paid=True, checking_id=checking_id)
return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK
return jsonify({"paid": False}), HTTPStatus.OK
@events_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"])
@api_check_wallet_key("invoice")
def api_ticket_delete(ticket_id):
ticket = get_ticket(ticket_id)
if not ticket:
return jsonify({"message": "Ticket does not exist."}), HTTPStatus.NOT_FOUND
if ticket.wallet != g.wallet.id:
return jsonify({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN
delete_ticket(ticket_id)
return "", HTTPStatus.NO_CONTENT
#########EventTickets##########
@events_ext.route("/api/v1/eventtickets/<wallet_id>/<event_id>", methods=["GET"])
def api_event_tickets(wallet_id, event_id):
return jsonify([ticket._asdict() for ticket in get_event_tickets(wallet_id=wallet_id, event_id=event_id)]), HTTPStatus.OK
@events_ext.route("/api/v1/register/ticket/<ticket_id>", methods=["GET"])
def api_event_register_ticket(ticket_id):
ticket = get_ticket(ticket_id)
if not ticket:
return jsonify({"message": "Ticket does not exist."}), HTTPStatus.FORBIDDEN
if ticket.registered == True:
return jsonify({"message": "Ticket already registered"}), HTTPStatus.FORBIDDEN
return jsonify([ticket._asdict() for ticket in reg_ticket(ticket_id)]), HTTPStatus.OK
return jsonify({"status": "TRUE", "name": eventssold[0][3]}), 200
@@ -1,54 +1,57 @@
{% extends "base.html" %}
{% from "macros.jinja" import window_vars with context %}
{% block page %}
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-mt-none q-mb-md">Frameworks used by LNbits</h5>
<q-list>
<q-item v-for="tool in tools" :key="tool.name" tag="a" :href="tool.url" target="_blank">
{% raw %} <!-- with raw Flask won't try to interpret the Vue moustaches -->
<q-item-section>
<q-item-label>{{ tool.name }}</q-item-label>
<q-item-label caption>{{ tool.language }}</q-item-label>
</q-item-section>
{% endraw %}
</q-item>
</q-list>
<q-separator class="q-my-lg"></q-separator>
<p>A magical "g" is always available, with info about the user, wallets and extensions:</p>
<code class="text-caption">{% raw %}{{ g }}{% endraw %}</code>
</q-card-section>
</q-card>
{% endblock %}
{% block scripts %}
{{ window_vars(user) }}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tools: []
};
},
created: function () {
var self = this;
// axios is available for making requests
axios({
method: 'GET',
url: '/example/api/v1/tools',
headers: {
'X-example-header': 'not-used'
}
}).then(function (response) {
self.tools = response.data;
});
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-mt-none q-mb-md">Frameworks used by LNbits</h5>
<q-list>
<q-item
v-for="tool in tools"
:key="tool.name"
tag="a"
:href="tool.url"
target="_blank"
>
{% raw %}
<!-- with raw Flask won't try to interpret the Vue moustaches -->
<q-item-section>
<q-item-label>{{ tool.name }}</q-item-label>
<q-item-label caption>{{ tool.language }}</q-item-label>
</q-item-section>
{% endraw %}
</q-item>
</q-list>
<q-separator class="q-my-lg"></q-separator>
<p>
A magical "g" is always available, with info about the user, wallets and
extensions:
</p>
<code class="text-caption">{% raw %}{{ g }}{% endraw %}</code>
</q-card-section>
</q-card>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tools: []
}
});
</script>
},
created: function () {
var self = this
// axios is available for making requests
axios({
method: 'GET',
url: '/example/api/v1/tools',
headers: {
'X-example-header': 'not-used'
}
}).then(function (response) {
self.tools = response.data
})
}
})
</script>
{% endblock %}
+2 -2
View File
@@ -6,8 +6,8 @@
# import requests
from flask import jsonify
from http import HTTPStatus
from lnbits.helpers import Status
from lnbits.extensions.example import example_ext
@@ -34,4 +34,4 @@ def api_example():
}
]
return jsonify(tools), Status.OK
return jsonify(tools), HTTPStatus.OK
+3
View File
@@ -0,0 +1,3 @@
<h1>Support Tickets</h1>
<h2>Get paid sats to answer questions</h2>
Charge people per word for contacting you. Possible applications include, paid support ticketing, PAYG language services, contact spam protection.
+8
View File
@@ -0,0 +1,8 @@
from flask import Blueprint
lnticket_ext: Blueprint = Blueprint("lnticket", __name__, static_folder="static", template_folder="templates")
from .views_api import * # noqa
from .views import * # noqa
+6
View File
@@ -0,0 +1,6 @@
{
"name": "Support Tickets",
"short_description": "LN support ticket system",
"icon": "contact_support",
"contributors": ["benarc"]
}
+121
View File
@@ -0,0 +1,121 @@
from typing import List, Optional, Union
from lnbits.db import open_ext_db
from lnbits.helpers import urlsafe_short_hash
from .models import Tickets, Forms
#######TICKETS########
def create_ticket(checking_id: str, wallet: str, form: str, name: str, email: str, ltext: str, sats: int) -> Tickets:
with open_ext_db("lnticket") as db:
db.execute(
"""
INSERT INTO ticket (id, form, email, ltext, name, wallet, sats, paid)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(checking_id, form, email, ltext, name, wallet, sats, False),
)
return get_ticket(checking_id)
def update_ticket(paid: bool, checking_id: str) -> Tickets:
with open_ext_db("lnticket") as db:
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (checking_id,))
if row[7] == True:
return get_ticket(checking_id)
db.execute(
"""
UPDATE ticket
SET paid = ?
WHERE id = ?
""",
(paid, checking_id),
)
formdata = get_form(row[1])
amount = formdata.amountmade + row[7]
db.execute(
"""
UPDATE forms
SET amountmade = ?
WHERE id = ?
""",
(amount, row[1]),
)
return get_ticket(checking_id)
def get_ticket(ticket_id: str) -> Optional[Tickets]:
with open_ext_db("lnticket") as db:
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (ticket_id,))
return Tickets(**row) if row else None
def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("lnticket") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM ticket WHERE wallet IN ({q})", (*wallet_ids,))
return [Tickets(**row) for row in rows]
def delete_ticket(ticket_id: str) -> None:
with open_ext_db("lnticket") as db:
db.execute("DELETE FROM ticket WHERE id = ?", (ticket_id,))
########FORMS#########
def create_form(*, wallet: str, name: str, description: str, costpword: int) -> Forms:
with open_ext_db("lnticket") as db:
form_id = urlsafe_short_hash()
db.execute(
"""
INSERT INTO forms (id, wallet, name, description, costpword, amountmade)
VALUES (?, ?, ?, ?, ?, ?)
""",
(form_id, wallet, name, description, costpword, 0 ),
)
return get_form(form_id)
def update_form(form_id: str, **kwargs) -> Forms:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("lnticket") as db:
db.execute(f"UPDATE forms SET {q} WHERE id = ?", (*kwargs.values(), form_id))
row = db.fetchone("SELECT * FROM forms WHERE id = ?", (form_id,))
return Forms(**row) if row else None
def get_form(form_id: str) -> Optional[Forms]:
with open_ext_db("lnticket") as db:
row = db.fetchone("SELECT * FROM forms WHERE id = ?", (form_id,))
return Forms(**row) if row else None
def get_forms(wallet_ids: Union[str, List[str]]) -> List[Forms]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("lnticket") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM forms WHERE wallet IN ({q})", (*wallet_ids,))
return [Forms(**row) for row in rows]
def delete_form(form_id: str) -> None:
with open_ext_db("lnticket") as db:
db.execute("DELETE FROM forms WHERE id = ?", (form_id,))
+94
View File
@@ -0,0 +1,94 @@
from lnbits.db import open_ext_db
def m001_initial(db):
db.execute(
"""
CREATE TABLE IF NOT EXISTS forms (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT NOT NULL,
costpword INTEGER NOT NULL,
amountmade INTEGER NOT NULL,
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
);
"""
)
db.execute(
"""
CREATE TABLE IF NOT EXISTS tickets (
id TEXT PRIMARY KEY,
form TEXT NOT NULL,
email TEXT NOT NULL,
ltext TEXT NOT NULL,
name TEXT NOT NULL,
wallet TEXT NOT NULL,
sats INTEGER NOT NULL,
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
);
"""
)
def m002_changed(db):
db.execute(
"""
CREATE TABLE IF NOT EXISTS ticket (
id TEXT PRIMARY KEY,
form TEXT NOT NULL,
email TEXT NOT NULL,
ltext TEXT NOT NULL,
name TEXT NOT NULL,
wallet TEXT NOT NULL,
sats INTEGER NOT NULL,
paid BOOLEAN NOT NULL,
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
);
"""
)
for row in [list(row) for row in db.fetchall("SELECT * FROM tickets")]:
usescsv = ""
for i in range(row[5]):
if row[7]:
usescsv += "," + str(i + 1)
else:
usescsv += "," + str(1)
usescsv = usescsv[1:]
db.execute(
"""
INSERT INTO ticket (
id,
form,
email,
ltext,
name,
wallet,
sats,
paid
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
row[0],
row[1],
row[2],
row[3],
row[4],
row[5],
row[6],
True,
),
)
db.execute("DROP TABLE tickets")
def migrate():
with open_ext_db("lnticket") as db:
m001_initial(db)
m002_changed(db)
+24
View File
@@ -0,0 +1,24 @@
from typing import NamedTuple
class Forms(NamedTuple):
id: str
wallet: str
name: str
description: str
costpword: int
amountmade: int
time: int
class Tickets(NamedTuple):
id: str
form: str
email: str
ltext: str
name: str
wallet: str
sats: int
paid: bool
time: int
@@ -0,0 +1,22 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">
Support Tickets: Get paid sats to answer questions
</h5>
<p>
Charge people per word for contacting you. Possible applications incude,
paid support ticketing, PAYG language services, contact spam
protection.<br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -0,0 +1,204 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<h3 class="q-my-none">{{ form_name }}</h3>
<br />
<h5 class="q-my-none">{{ form_desc }}</h5>
<br />
<q-form @submit="Invoice()" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialog.data.name"
type="name"
label="Your name "
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.email"
type="email"
label="Your email (optional, if you want a reply)"
></q-input>
<q-input
filled
dense
v-model.number="formDialog.data.text"
type="textarea"
label="{{ form_costpword }} sats per word"
></q-input>
<p>{% raw %}{{amountWords}}{% endraw %}</p>
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
:disable="formDialog.data.name == '' || formDialog.data.text == ''"
type="submit"
>Submit</q-btn
>
<q-btn @click="resetForm" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
<q-card
v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="paymentReq"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
console.log('{{ form_costpword }}')
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
paymentReq: null,
redirectUrl: null,
formDialog: {
show: false,
data: {
name: '',
email: '',
text: ''
}
},
receive: {
show: false,
status: 'pending',
paymentReq: null
}
}
},
computed: {
amountWords() {
var regex = /\s+/gi
var char = this.formDialog.data.text
.trim()
.replace(regex, ' ')
.split(' ').length
this.formDialog.data.sats = char * parseInt('{{ form_costpword }}')
if (this.formDialog.data.sats == parseInt('{{ form_costpword }}')) {
return '0 Sats to pay'
} else {
return this.formDialog.data.sats + ' Sats to pay'
}
}
},
methods: {
resetForm: function (e) {
e.preventDefault()
this.formDialog.data.name = ''
this.formDialog.data.email = ''
this.formDialog.data.text = ''
this.formDialog.data.sats = 0
},
closeReceiveDialog: function () {
var checker = this.receive.paymentChecker
dismissMsg()
clearInterval(paymentChecker)
setTimeout(function () {}, 10000)
},
Invoice: function () {
var self = this
axios
.post(
'/lnticket/api/v1/tickets/' +
'{{ form_id }}/' +
self.formDialog.data.sats,
{
form: '{{ form_id }}',
name: self.formDialog.data.name,
email: self.formDialog.data.email,
ltext: self.formDialog.data.text,
sats: self.formDialog.data.sats
}
)
.then(function (response) {
self.paymentReq = response.data.payment_request
self.paymentCheck = response.data.checking_id
dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.receive = {
show: true,
status: 'pending',
paymentReq: self.paymentReq
}
paymentChecker = setInterval(function () {
axios
.get('/lnticket/api/v1/tickets/' + self.paymentCheck)
.then(function (res) {
if (res.data.paid) {
clearInterval(paymentChecker)
dismissMsg()
self.formDialog.data.name = ''
self.formDialog.data.email = ''
self.formDialog.data.text = ''
self.formDialog.data.sats = 0
self.$q.notify({
type: 'positive',
message: 'Sent, thank you!',
icon: null
})
self.receive = {
show: false,
status: 'complete',
paymentReq: null
}
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}, 2000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
}
})
</script>
{% endblock %}
@@ -0,0 +1,443 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
>New Form</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Forms</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportformsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="forms"
row-key="id"
:columns="formsTable.columns"
:pagination.sync="formsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="link"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.displayUrl"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="updateformDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteForm(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Tickets</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportticketsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="tickets"
row-key="id"
:columns="ticketsTable.columns"
:pagination.sync="ticketsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="email"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'mailto:' + props.row.email"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteTicket(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
LNbits Support Tickets extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "lnticket/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
v-model.trim="formDialog.data.name"
type="name"
label="Form name "
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.description"
type="textarea"
label="Description "
></q-input>
<q-input
filled
dense
v-model.number="formDialog.data.costpword"
type="number"
label="Amount per word"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="formDialog.data.id"
unelevated
color="deep-purple"
type="submit"
>Update Form</q-btn
>
<q-btn
v-else
unelevated
color="deep-purple"
:disable="formDialog.data.costpword == null || formDialog.data.costpword < 0 || formDialog.data.name == null"
type="submit"
>Create Form</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapLNTicket = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.displayUrl = ['/lnticket/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
forms: [],
tickets: [],
formsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
{
name: 'description',
align: 'left',
label: 'Description',
field: 'description'
},
{
name: 'costpword',
align: 'left',
label: 'Cost Per Word',
field: 'costpword'
},
{
name: 'amountmade',
align: 'left',
label: 'Amount Made',
field: 'amountmade'
}
],
pagination: {
rowsPerPage: 10
}
},
ticketsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'email', align: 'left', label: 'Email', field: 'email'},
{name: 'ltext', align: 'left', label: 'Ticket', field: 'ltext'},
{name: 'sats', align: 'left', label: 'Paid', field: 'sats'}
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {}
}
}
},
methods: {
getTickets: function () {
var self = this
LNbits.api
.request(
'GET',
'/lnticket/api/v1/tickets?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.tickets = response.data.map(function (obj) {
return mapLNTicket(obj)
})
})
},
deleteTicket: function (ticketId) {
var self = this
var tickets = _.findWhere(this.tickets, {id: ticketId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this ticket')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/lnticket/api/v1/tickets/' + ticketId,
_.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey
)
.then(function (response) {
self.tickets = _.reject(self.tickets, function (obj) {
return obj.id == ticketId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportticketsCSV: function () {
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
},
getForms: function () {
var self = this
LNbits.api
.request(
'GET',
'/lnticket/api/v1/forms?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.forms = response.data.map(function (obj) {
return mapLNTicket(obj)
})
})
},
sendFormData: function () {
var wallet = _.findWhere(this.g.user.wallets, {
id: this.formDialog.data.wallet
})
var data = this.formDialog.data
if (data.id) {
this.updateForm(wallet, data)
} else {
this.createForm(wallet, data)
}
},
createForm: function (wallet, data) {
var self = this
LNbits.api
.request('POST', '/lnticket/api/v1/forms', wallet.inkey, data)
.then(function (response) {
self.forms.push(mapLNTicket(response.data))
self.formDialog.show = false
self.formDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
updateformDialog: function (formId) {
var link = _.findWhere(this.forms, {id: formId})
console.log(link.id)
this.formDialog.data.id = link.id
this.formDialog.data.wallet = link.wallet
this.formDialog.data.name = link.name
this.formDialog.data.description = link.description
this.formDialog.data.costpword = link.costpword
this.formDialog.show = true
},
updateForm: function (wallet, data) {
var self = this
console.log(data)
LNbits.api
.request(
'PUT',
'/lnticket/api/v1/forms/' + data.id,
wallet.inkey,
data
)
.then(function (response) {
self.forms = _.reject(self.forms, function (obj) {
return obj.id == data.id
})
self.forms.push(mapLNTicket(response.data))
self.formDialog.show = false
self.formDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteForm: function (formsId) {
var self = this
var forms = _.findWhere(this.forms, {id: formsId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this form link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/lnticket/api/v1/forms/' + formsId,
_.findWhere(self.g.user.wallets, {id: forms.wallet}).inkey
)
.then(function (response) {
self.forms = _.reject(self.forms, function (obj) {
return obj.id == formsId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportformsCSV: function () {
LNbits.utils.exportCSV(this.formsTable.columns, this.forms)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getTickets()
this.getForms()
}
}
})
</script>
{% endblock %}
+22
View File
@@ -0,0 +1,22 @@
from flask import g, abort, render_template
from lnbits.decorators import check_user_exists, validate_uuids
from http import HTTPStatus
from lnbits.extensions.lnticket import lnticket_ext
from .crud import get_form
@lnticket_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
def index():
return render_template("lnticket/index.html", user=g.user)
@lnticket_ext.route("/<form_id>")
def display(form_id):
form = get_form(form_id) or abort(HTTPStatus.NOT_FOUND, "LNTicket does not exist.")
print(form.id)
return render_template("lnticket/display.html", form_id=form.id, form_name=form.name, form_desc=form.description, form_costpword=form.costpword)
+143
View File
@@ -0,0 +1,143 @@
from flask import g, jsonify, request
from http import HTTPStatus
from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.settings import WALLET
from lnbits.extensions.lnticket import lnticket_ext
from .crud import create_ticket, update_ticket, get_ticket, get_tickets, delete_ticket, create_form, update_form, get_form, get_forms, delete_form
#########FORMS##########
@lnticket_ext.route("/api/v1/forms", methods=["GET"])
@api_check_wallet_key("invoice")
def api_forms():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([form._asdict() for form in get_forms(wallet_ids)]), HTTPStatus.OK
@lnticket_ext.route("/api/v1/forms", methods=["POST"])
@lnticket_ext.route("/api/v1/forms/<form_id>", methods=["PUT"])
@api_check_wallet_key("invoice")
@api_validate_post_request(
schema={
"wallet": {"type": "string", "empty": False, "required": True},
"name": {"type": "string", "empty": False, "required": True},
"description": {"type": "string", "min": 0, "required": True},
"costpword": {"type": "integer", "min": 0, "required": True}
}
)
def api_form_create(form_id=None):
if form_id:
form = get_form(form_id)
print(g.data)
if not form:
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
if form.wallet != g.wallet.id:
return jsonify({"message": "Not your form."}), HTTPStatus.FORBIDDEN
form = update_form(form_id, **g.data)
else:
form = create_form(**g.data)
return jsonify(form._asdict()), HTTPStatus.CREATED
@lnticket_ext.route("/api/v1/forms/<form_id>", methods=["DELETE"])
@api_check_wallet_key("invoice")
def api_form_delete(form_id):
form = get_form(form_id)
if not form:
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
if form.wallet != g.wallet.id:
return jsonify({"message": "Not your form."}), HTTPStatus.FORBIDDEN
delete_form(form_id)
return "", HTTPStatus.NO_CONTENT
#########tickets##########
@lnticket_ext.route("/api/v1/tickets", methods=["GET"])
@api_check_wallet_key("invoice")
def api_tickets():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([form._asdict() for form in get_tickets(wallet_ids)]), HTTPStatus.OK
@lnticket_ext.route("/api/v1/tickets/<form_id>/<sats>", methods=["POST"])
@api_validate_post_request(
schema={
"form": {"type": "string", "empty": False, "required": True},
"name": {"type": "string", "empty": False, "required": True},
"email": {"type": "string", "empty": False, "required": True},
"ltext": {"type": "string", "empty": False, "required": True},
"sats": {"type": "integer", "min": 0, "required": True}
})
def api_ticket_make_ticket(form_id, sats):
event = get_form(form_id)
if not event:
return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND
try:
checking_id, payment_request = create_invoice(
wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {form_id}"
)
except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
ticket = create_ticket(checking_id=checking_id, wallet=event.wallet, **g.data)
if not ticket:
return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK
@lnticket_ext.route("/api/v1/tickets/<checking_id>", methods=["GET"])
def api_ticket_send_ticket(checking_id):
theticket = get_ticket(checking_id)
try:
is_paid = not WALLET.get_invoice_status(checking_id).pending
except Exception:
return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND
if is_paid:
wallet = get_wallet(theticket.wallet)
payment = wallet.get_payment(checking_id)
payment.set_pending(False)
ticket = update_ticket(paid=True, checking_id=checking_id)
return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK
return jsonify({"paid": False}), HTTPStatus.OK
@lnticket_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"])
@api_check_wallet_key("invoice")
def api_ticket_delete(ticket_id):
ticket = get_ticket(ticket_id)
if not ticket:
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
if ticket.wallet != g.wallet.id:
return jsonify({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN
delete_ticket(ticket_id)
return "", HTTPStatus.NO_CONTENT
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Paywall",
"short_description": "Create paywalls for content",
"icon": "vpn_lock",
"icon": "policy",
"contributors": ["eillarra"]
}
+8 -6
View File
@@ -6,15 +6,17 @@ from lnbits.helpers import urlsafe_short_hash
from .models import Paywall
def create_paywall(*, wallet_id: str, url: str, memo: str, amount: int) -> Paywall:
def create_paywall(
*, wallet_id: str, url: str, memo: str, description: Optional[str] = None, amount: int = 0, remembers: bool = True
) -> Paywall:
with open_ext_db("paywall") as db:
paywall_id = urlsafe_short_hash()
db.execute(
"""
INSERT INTO paywalls (id, wallet, secret, url, memo, amount)
VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO paywalls (id, wallet, url, memo, description, amount, remembers)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(paywall_id, wallet_id, urlsafe_short_hash(), url, memo, amount),
(paywall_id, wallet_id, url, memo, description, amount, int(remembers)),
)
return get_paywall(paywall_id)
@@ -24,7 +26,7 @@ def get_paywall(paywall_id: str) -> Optional[Paywall]:
with open_ext_db("paywall") as db:
row = db.fetchone("SELECT * FROM paywalls WHERE id = ?", (paywall_id,))
return Paywall(**row) if row else None
return Paywall.from_row(row) if row else None
def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
@@ -35,7 +37,7 @@ def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM paywalls WHERE wallet IN ({q})", (*wallet_ids,))
return [Paywall(**row) for row in rows]
return [Paywall.from_row(row) for row in rows]
def delete_paywall(paywall_id: str) -> None:
+48
View File
@@ -1,3 +1,5 @@
from sqlite3 import OperationalError
from lnbits.db import open_ext_db
@@ -20,6 +22,52 @@ def m001_initial(db):
)
def m002_redux(db):
"""
Creates an improved paywalls table and migrates the existing data.
"""
try:
db.execute("SELECT remembers FROM paywalls")
except OperationalError:
db.execute("ALTER TABLE paywalls RENAME TO paywalls_old")
db.execute(
"""
CREATE TABLE IF NOT EXISTS paywalls (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
url TEXT NOT NULL,
memo TEXT NOT NULL,
description TEXT NULL,
amount INTEGER DEFAULT 0,
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')),
remembers INTEGER DEFAULT 0,
extras TEXT NULL
);
"""
)
db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON paywalls (wallet)")
for row in [list(row) for row in db.fetchall("SELECT * FROM paywalls_old")]:
db.execute(
"""
INSERT INTO paywalls (
id,
wallet,
url,
memo,
amount,
time
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(row[0], row[1], row[3], row[4], row[5], row[6]),
)
db.execute("DROP TABLE paywalls_old")
def migrate():
with open_ext_db("paywall") as db:
m001_initial(db)
m002_redux(db)
+14 -2
View File
@@ -1,11 +1,23 @@
from typing import NamedTuple
import json
from sqlite3 import Row
from typing import NamedTuple, Optional
class Paywall(NamedTuple):
id: str
wallet: str
secret: str
url: str
memo: str
description: str
amount: int
time: int
remembers: bool
extras: Optional[dict]
@classmethod
def from_row(cls, row: Row) -> "Paywall":
data = dict(row)
data["remembers"] = bool(data["remembers"])
data["extras"] = json.loads(data["extras"]) if data["extras"] else None
return cls(**data)
@@ -7,21 +7,140 @@
<q-expansion-item group="api" dense expand-separator label="List paywalls">
<q-card>
<q-card-section>
<code><span class="text-blue">GET</span> /paywall/api/v1/paywalls</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;paywall_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}paywall/api/v1/paywalls -H
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Create a paywall">
<q-card>
<q-card-section>
<code
><span class="text-green">POST</span> /paywall/api/v1/paywalls</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"amount": &lt;integer&gt;, "description": &lt;string&gt;, "memo":
&lt;string&gt;, "remembers": &lt;boolean&gt;, "url":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"amount": &lt;integer&gt;, "description": &lt;string&gt;, "id":
&lt;string&gt;, "memo": &lt;string&gt;, "remembers": &lt;boolean&gt;,
"time": &lt;int&gt;, "url": &lt;string&gt;, "wallet":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}paywall/api/v1/paywalls -d
'{"url": &lt;string&gt;, "memo": &lt;string&gt;, "description":
&lt;string&gt;, "amount": &lt;integer&gt;, "remembers":
&lt;boolean&gt;}' -H "Content-type: application/json" -H "X-Api-Key:
{{ g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Delete a paywall" class="q-pb-md">
<q-expansion-item
group="api"
dense
expand-separator
label="Create an invoice (public)"
>
<q-card>
<q-card-section>
<code
><span class="text-green">POST</span>
/paywall/api/v1/paywalls/&lt;paywall_id&gt;/invoice</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"amount": &lt;integer&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"checking_id": &lt;string&gt;, "payment_request":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root
}}paywall/api/v1/paywalls/&lt;paywall_id&gt;/invoice -d '{"amount":
&lt;integer&gt;}' -H "Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Check invoice status (public)"
>
<q-card>
<q-card-section>
<code
><span class="text-green">POST</span>
/paywall/api/v1/paywalls/&lt;paywall_id&gt;/check_invoice</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"checking_id": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{"paid": false}</code><br />
<code
>{"paid": true, "url": &lt;string&gt;, "remembers":
&lt;boolean&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root
}}paywall/api/v1/paywalls/&lt;paywall_id&gt;/check_invoice -d
'{"checking_id": &lt;string&gt;}' -H "Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Delete a paywall"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/paywall/api/v1/paywalls/&lt;paywall_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.url_root
}}paywall/api/v1/paywalls/&lt;paywall_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -1,107 +1,163 @@
{% extends "public.html" %}
{% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<h5 class="text-subtitle1 q-my-none">{{ paywall.memo }}</h5>
<strong class="text-purple">Price: <lnbits-fsat :amount="{{ paywall.amount }}"></lnbits-fsat> sat</strong>
<q-separator class="q-my-lg"></q-separator>
<div v-if="paymentReq">
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-sm-8 col-md-5 col-lg-4">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<h5 class="text-subtitle1 q-mt-none q-mb-sm">{{ paywall.memo }}</h5>
{% if paywall.description %}
<p>{{ paywall.description }}</p>
{% endif %}
<div v-if="!this.redirectUrl" class="q-mt-lg">
<q-form v-if="">
<q-input
filled
v-model.number="userAmount"
type="number"
:min="paywallAmount"
suffix="sat"
label="Choose an amount *"
:hint="'Minimum ' + paywallAmount + ' sat'"
>
<template v-slot:after>
<q-btn
round
dense
flat
icon="check"
color="deep-purple"
type="submit"
@click="createInvoice"
:disabled="userAmount < paywallAmount || paymentReq"
></q-btn>
</template>
</q-input>
</q-form>
<div v-if="paymentReq" class="q-mt-lg">
<a :href="'lightning:' + paymentReq">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode :value="paymentReq" :options="{width: 800}" class="rounded-borders"></qrcode>
<qrcode
:value="paymentReq"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(paymentReq)">Copy invoice</q-btn>
<q-btn outline color="grey" @click="copyText(paymentReq)"
>Copy invoice</q-btn
>
<q-btn @click="cancelPayment" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</div>
<div v-if="redirectUrl">
<p>You can access the URL behind this paywall:<br>
<strong>{% raw %}{{ redirectUrl }}{% endraw %}</strong></p>
<div class="row q-mt-lg">
<q-btn outline color="grey" type="a" :href="redirectUrl">Open URL</q-btn>
</div>
</div>
<div v-else>
<q-separator class="q-my-lg"></q-separator>
<p>
You can access the URL behind this paywall:<br />
<strong>{% raw %}{{ redirectUrl }}{% endraw %}</strong>
</p>
<div class="row q-mt-lg">
<q-btn outline color="grey" type="a" :href="redirectUrl"
>Open URL</q-btn
>
</div>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mb-sm q-mt-none">LNbits paywall</h6>
</q-card-section>
</q-card>
</div>
</div>
</q-card-section>
</q-card>
</div>
{% endblock %}
</div>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode)
{% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode);
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
paymentReq: null,
redirectUrl: null
};
},
methods: {
getInvoice: function () {
var self = this;
axios.get(
'/paywall/api/v1/paywalls/{{ paywall.id }}/invoice'
).then(function (response) {
self.paymentReq = response.data.payment_request;
dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
});
paymentChecker = setInterval(function () {
axios.post(
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice',
{checking_id: response.data.checking_id}
).then(function (res) {
if (res.data.paid) {
clearInterval(paymentChecker);
dismissMsg();
self.redirectUrl = res.data.url;
self.$q.localStorage.set('lnbits.paywall.{{ paywall.id }}', res.data.url);
self.$q.notify({
type: 'positive',
message: 'Payment received!',
icon: null
});
}
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
}, 2000);
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
userAmount: {{ paywall.amount }},
paywallAmount: {{ paywall.amount }},
paymentReq: null,
redirectUrl: null,
paymentDialog: {
dismissMsg: null,
checker: null
}
}
},
computed: {
amount: function () {
return (this.paywallAmount > this.userAmount) ? this.paywallAmount : this.userAmount
}
},
methods: {
cancelPayment: function () {
this.paymentReq = null
clearInterval(this.paymentDialog.checker)
if (this.paymentDialog.dismissMsg) {
this.paymentDialog.dismissMsg()
}
},
created: function () {
var url = this.$q.localStorage.getItem('lnbits.paywall.{{ paywall.id }}');
createInvoice: function () {
var self = this
if (url) {
this.redirectUrl = url;
} else {
this.getInvoice();
};
axios
.post(
'/paywall/api/v1/paywalls/{{ paywall.id }}/invoice',
{amount: this.amount}
)
.then(function (response) {
self.paymentReq = response.data.payment_request.toUpperCase()
self.paymentDialog.dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.paymentDialog.checker = setInterval(function () {
axios
.post(
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice',
{checking_id: response.data.checking_id}
)
.then(function (res) {
if (res.data.paid) {
self.cancelPayment()
self.redirectUrl = res.data.url
if (res.data.remembers) {
self.$q.localStorage.set(
'lnbits.paywall.{{ paywall.id }}',
res.data.url
)
}
self.$q.notify({
type: 'positive',
message: 'Payment received!',
icon: null
})
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}, 2000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
});
</script>
},
created: function () {
var url = this.$q.localStorage.getItem('lnbits.paywall.{{ paywall.id }}')
if (url) {
this.redirectUrl = url
}
}
})
</script>
{% endblock %}
@@ -1,214 +1,312 @@
{% extends "base.html" %}
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
>New paywall</q-btn
>
</q-card-section>
</q-card>
{% from "macros.jinja" import window_vars with context %}
{% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true">New paywall</q-btn>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Paywalls</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Paywalls</h5>
</div>
<q-table dense flat
:data="paywalls"
row-key="id"
:columns="paywallsTable.columns"
:pagination.sync="paywallsTable.pagination">
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn unelevated dense size="xs" icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.displayUrl" target="_blank"></q-btn>
<q-btn unelevated dense size="xs" icon="link" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.url" target="_blank"></q-btn>
</q-td>
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn flat dense size="xs" @click="deletePaywall(props.row.id)" icon="cancel" color="pink"></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits paywall extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "paywall/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="createPaywall" class="q-gutter-md">
<q-select filled dense emit-value v-model="formDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *">
</q-select>
<q-input filled dense
v-model.trim="formDialog.data.url"
type="url"
label="Target URL *"></q-input>
<q-input filled dense
v-model.number="formDialog.data.amount"
type="number"
label="Amount (sat) *"></q-input>
<q-input filled dense
v-model.trim="formDialog.data.memo"
label="Memo"
placeholder="LNbits invoice"></q-input>
<div class="row q-mt-lg">
<q-btn unelevated
color="deep-purple"
:disable="formDialog.data.amount == null || formDialog.data.amount < 0 || formDialog.data.url == null"
type="submit">Create paywall</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
<q-table
dense
flat
:data="paywalls"
row-key="id"
:columns="paywallsTable.columns"
:pagination.sync="paywallsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.displayUrl"
target="_blank"
></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="link"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.url"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deletePaywall(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
{% endblock %}
{% block scripts %}
{{ window_vars(user) }}
<script>
var mapPaywall = function (obj) {
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm');
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount);
obj.displayUrl = ['/paywall/', obj.id].join('');
return obj;
}
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits paywall extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "paywall/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
paywalls: [],
paywallsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
{name: 'date', align: 'left', label: 'Date', field: 'date', sortable: true},
{
name: 'amount', align: 'right', label: 'Amount (sat)', field: 'fsat', sortable: true,
sort: function (a, b, rowA, rowB) {
return rowA.amount - rowB.amount;
}
<q-dialog v-model="formDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="createPaywall" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
v-model.trim="formDialog.data.url"
type="url"
label="Redirect URL *"
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.memo"
label="Title *"
placeholder="LNbits paywall"
></q-input>
<q-input
filled
dense
autogrow
v-model.trim="formDialog.data.description"
label="Description"
></q-input>
<q-input
filled
dense
v-model.number="formDialog.data.amount"
type="number"
label="Amount (sat) *"
hint="This is the minimum amount users can pay/donate."
></q-input>
<q-list>
<q-item tag="label" class="rounded-borders">
<q-item-section avatar>
<q-checkbox
v-model="formDialog.data.remembers"
color="deep-purple"
></q-checkbox>
</q-item-section>
<q-item-section>
<q-item-label>Remember payments</q-item-label>
<q-item-label caption
>A succesful payment will be registered in the browser's
storage, so the user doesn't need to pay again to access the
URL.</q-item-label
>
</q-item-section>
</q-item>
</q-list>
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
:disable="formDialog.data.amount == null || formDialog.data.amount < 0 || formDialog.data.url == null || formDialog.data.memo == null"
type="submit"
>Create paywall</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapPaywall = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.displayUrl = ['/paywall/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
paywalls: [],
paywallsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
{
name: 'amount',
align: 'right',
label: 'Amount (sat)',
field: 'fsat',
sortable: true,
sort: function (a, b, rowA, rowB) {
return rowA.amount - rowB.amount
}
],
pagination: {
rowsPerPage: 10
},
{
name: 'remembers',
align: 'left',
label: 'Remember',
field: 'remembers'
},
{
name: 'date',
align: 'left',
label: 'Date',
field: 'date',
sortable: true
}
},
formDialog: {
show: false,
data: {}
],
pagination: {
rowsPerPage: 10
}
};
},
methods: {
getPaywalls: function () {
var self = this;
},
formDialog: {
show: false,
data: {
remembers: false
}
}
}
},
methods: {
getPaywalls: function () {
var self = this
LNbits.api.request(
LNbits.api
.request(
'GET',
'/paywall/api/v1/paywalls?all_wallets',
this.g.user.wallets[0].inkey
).then(function (response) {
)
.then(function (response) {
self.paywalls = response.data.map(function (obj) {
return mapPaywall(obj);
});
});
},
createPaywall: function () {
var data = {
url: this.formDialog.data.url,
memo: this.formDialog.data.memo,
amount: this.formDialog.data.amount
};
var self = this;
return mapPaywall(obj)
})
})
},
createPaywall: function () {
var data = {
url: this.formDialog.data.url,
memo: this.formDialog.data.memo,
amount: this.formDialog.data.amount,
description: this.formDialog.data.description,
remembers: this.formDialog.data.remembers
}
var self = this
LNbits.api.request(
LNbits.api
.request(
'POST',
'/paywall/api/v1/paywalls',
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet}).inkey,
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
.inkey,
data
).then(function (response) {
self.paywalls.push(mapPaywall(response.data));
self.formDialog.show = false;
self.formDialog.data = {};
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
},
deletePaywall: function (paywallId) {
var self = this;
var paywall = _.findWhere(this.paywalls, {id: paywallId});
LNbits.utils.confirmDialog(
'Are you sure you want to delete this paywall link?'
).onOk(function () {
LNbits.api.request(
'DELETE',
'/paywall/api/v1/paywalls/' + paywallId,
_.findWhere(self.g.user.wallets, {id: paywall.wallet}).inkey
).then(function (response) {
self.paywalls = _.reject(self.paywalls, function (obj) { return obj.id == paywallId; });
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
});
},
exportCSV: function () {
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls);
}
)
.then(function (response) {
self.paywalls.push(mapPaywall(response.data))
self.formDialog.show = false
self.formDialog.data = {
remembers: false
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
created: function () {
if (this.g.user.wallets.length) {
this.getPaywalls();
}
deletePaywall: function (paywallId) {
var self = this
var paywall = _.findWhere(this.paywalls, {id: paywallId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this paywall link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/paywall/api/v1/paywalls/' + paywallId,
_.findWhere(self.g.user.wallets, {id: paywall.wallet}).inkey
)
.then(function (response) {
self.paywalls = _.reject(self.paywalls, function (obj) {
return obj.id == paywallId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportCSV: function () {
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls)
}
});
</script>
},
created: function () {
if (this.g.user.wallets.length) {
this.getPaywalls()
}
}
})
</script>
{% endblock %}
+2 -2
View File
@@ -1,7 +1,7 @@
from flask import g, abort, render_template
from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.helpers import Status
from lnbits.extensions.paywall import paywall_ext
from .crud import get_paywall
@@ -16,6 +16,6 @@ def index():
@paywall_ext.route("/<paywall_id>")
def display(paywall_id):
paywall = get_paywall(paywall_id) or abort(Status.NOT_FOUND, "Paywall does not exist.")
paywall = get_paywall(paywall_id) or abort(HTTPStatus.NOT_FOUND, "Paywall does not exist.")
return render_template("paywall/display.html", paywall=paywall)
+22 -15
View File
@@ -1,9 +1,9 @@
from flask import g, jsonify, request
from http import HTTPStatus
from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.helpers import Status
from lnbits.settings import WALLET
from lnbits.extensions.paywall import paywall_ext
@@ -18,7 +18,7 @@ def api_paywalls():
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([paywall._asdict() for paywall in get_paywalls(wallet_ids)]), Status.OK
return jsonify([paywall._asdict() for paywall in get_paywalls(wallet_ids)]), HTTPStatus.OK
@paywall_ext.route("/api/v1/paywalls", methods=["POST"])
@@ -27,13 +27,15 @@ def api_paywalls():
schema={
"url": {"type": "string", "empty": False, "required": True},
"memo": {"type": "string", "empty": False, "required": True},
"description": {"type": "string", "empty": True, "nullable": True, "required": False},
"amount": {"type": "integer", "min": 0, "required": True},
"remembers": {"type": "boolean", "required": True},
}
)
def api_paywall_create():
paywall = create_paywall(wallet_id=g.wallet.id, **g.data)
return jsonify(paywall._asdict()), Status.CREATED
return jsonify(paywall._asdict()), HTTPStatus.CREATED
@paywall_ext.route("/api/v1/paywalls/<paywall_id>", methods=["DELETE"])
@@ -42,28 +44,33 @@ def api_paywall_delete(paywall_id):
paywall = get_paywall(paywall_id)
if not paywall:
return jsonify({"message": "Paywall does not exist."}), Status.NOT_FOUND
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
if paywall.wallet != g.wallet.id:
return jsonify({"message": "Not your paywall."}), Status.FORBIDDEN
return jsonify({"message": "Not your paywall."}), HTTPStatus.FORBIDDEN
delete_paywall(paywall_id)
return "", Status.NO_CONTENT
return "", HTTPStatus.NO_CONTENT
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/invoice", methods=["GET"])
def api_paywall_get_invoice(paywall_id):
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/invoice", methods=["POST"])
@api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}})
def api_paywall_create_invoice(paywall_id):
paywall = get_paywall(paywall_id)
if g.data["amount"] < paywall.amount:
return jsonify({"message": f"Minimum amount is {paywall.amount} sat."}), HTTPStatus.BAD_REQUEST
try:
amount = g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount
checking_id, payment_request = create_invoice(
wallet_id=paywall.wallet, amount=paywall.amount, memo=f"#paywall {paywall.memo}"
wallet_id=paywall.wallet, amount=amount, memo=f"#paywall {paywall.memo}"
)
except Exception as e:
return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.OK
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/check_invoice", methods=["POST"])
@@ -72,18 +79,18 @@ def api_paywal_check_invoice(paywall_id):
paywall = get_paywall(paywall_id)
if not paywall:
return jsonify({"message": "Paywall does not exist."}), Status.NOT_FOUND
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
try:
is_paid = not WALLET.get_invoice_status(g.data["checking_id"]).pending
except Exception:
return jsonify({"paid": False}), Status.OK
return jsonify({"paid": False}), HTTPStatus.OK
if is_paid:
wallet = get_wallet(paywall.wallet)
payment = wallet.get_payment(g.data["checking_id"])
payment.set_pending(False)
return jsonify({"paid": True, "url": paywall.url}), Status.OK
return jsonify({"paid": True, "url": paywall.url, "remembers": paywall.remembers}), HTTPStatus.OK
return jsonify({"paid": False}), Status.OK
return jsonify({"paid": False}), HTTPStatus.OK
+2 -2
View File
@@ -24,7 +24,7 @@ def get_tpos(tpos_id: str) -> Optional[TPoS]:
with open_ext_db("tpos") as db:
row = db.fetchone("SELECT * FROM tposs WHERE id = ?", (tpos_id,))
return TPoS(**row) if row else None
return TPoS.from_row(row) if row else None
def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]:
@@ -35,7 +35,7 @@ def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM tposs WHERE wallet IN ({q})", (*wallet_ids,))
return [TPoS(**row) for row in rows]
return [TPoS.from_row(row) for row in rows]
def delete_tpos(tpos_id: str) -> None:
+5
View File
@@ -1,3 +1,4 @@
from sqlite3 import Row
from typing import NamedTuple
@@ -6,3 +7,7 @@ class TPoS(NamedTuple):
wallet: str
name: str
currency: str
@classmethod
def from_row(cls, row: Row) -> "TPoS":
return cls(**dict(row))
@@ -4,49 +4,74 @@
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="List all users TPoS">
<q-expansion-item group="api" dense expand-separator label="List TPoS">
<q-card>
<q-card-section>
<code><span class="text-light-blue">GET</span> /tpos/api/v1/tposs</code>
<code><span class="text-blue">GET</span> /tpos/api/v1/tposs</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5>
<code>{"currency": &lt;string&gt;, "id": &lt;string&gt;, "name": &lt;string&gt;, "wallet": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;tpos_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X GET {{ request.url_root }}tpos/api/v1/tposs -H "X-Api-Key: &lt;invoice_key&gt;" </code>
<code
>curl -X GET {{ request.url_root }}tpos/api/v1/tposs -H "X-Api-Key:
&lt;invoice_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Create a TPoS">
<q-card>
<q-card-section>
<code><span class="text-light-green">POST</span> /tpos/api/v1/tposs</code>
<code><span class="text-green">POST</span> /tpos/api/v1/tposs</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"name": &lt;string&gt;, "currency": &lt;string*ie USD*&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 CREATED (application/json)</h5>
<code>{"currency": &lt;string&gt;, "id": &lt;string&gt;, "name": &lt;string&gt;, "wallet": &lt;string&gt;}</code>
<code
>{"name": &lt;string&gt;, "currency": &lt;string*ie USD*&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"currency": &lt;string&gt;, "id": &lt;string&gt;, "name":
&lt;string&gt;, "wallet": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X POST {{ request.url_root }}tpos/api/v1/tposs -d '{"name": &lt;string&gt;, "currency": &lt;string&gt;}' -H "Content-type: application/json" -H "X-Api-Key: &lt;admin_key&gt;"
</code>
<code
>curl -X POST {{ request.url_root }}tpos/api/v1/tposs -d '{"name":
&lt;string&gt;, "currency": &lt;string&gt;}' -H "Content-type:
application/json" -H "X-Api-Key: &lt;admin_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Delete a TPoS" class="q-pb-md">
<q-expansion-item
group="api"
dense
expand-separator
label="Delete a TPoS"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code><span class="text-light-green">DELETE</span> /tpos/api/v1/tposs/&lt;tpos_id&gt;</code>
<code
><span class="text-pink">DELETE</span>
/tpos/api/v1/tposs/&lt;tpos_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 201 NO_CONTENT</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X DELETE {{ request.url_root }}tpos/api/v1/tposs/&lt;tpos_id&gt; -H "X-Api-Key: &lt;admin_key&gt;"
</code>
<code
>curl -X DELETE {{ request.url_root
}}tpos/api/v1/tposs/&lt;tpos_id&gt; -H "X-Api-Key: &lt;admin_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -1,11 +1,18 @@
<q-expansion-item
group="extras"
icon="info"
label="About TPoS">
<q-expansion-item group="extras" icon="info" label="About TPoS">
<q-card>
<q-card-section>
<p>Thiago's Point of Sale is a secure, mobile-ready, instant and shareable point of sale terminal (PoS) for merchants. The PoS is linked to your LNbits wallet but completely air-gapped so users can ONLY create invoices. To share the TPoS hit the hash on the terminal.</p>
<small>Created by <a href="https://github.com/talvasconcelos" target="_blank">Tiago Vasconcelos</a>.</small>
<p>
Thiago's Point of Sale is a secure, mobile-ready, instant and shareable
point of sale terminal (PoS) for merchants. The PoS is linked to your
LNbits wallet but completely air-gapped so users can ONLY create
invoices. To share the TPoS hit the hash on the terminal.
</p>
<small
>Created by
<a href="https://github.com/talvasconcelos" target="_blank"
>Tiago Vasconcelos</a
>.</small
>
</q-card-section>
</q-card>
</q-expansion-item>
+402 -200
View File
@@ -1,221 +1,423 @@
{% extends "base.html" %}
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
>New TPoS</q-btn
>
</q-card-section>
</q-card>
{% from "macros.jinja" import window_vars with context %}
{% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true">New TPoS</q-btn>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">TPoS</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">TPoS</h5>
</div>
<q-table dense flat
:data="tposs"
row-key="id"
:columns="tpossTable.columns"
:pagination.sync="tpossTable.pagination">
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:data="tposs"
row-key="id"
:columns="tpossTable.columns"
:pagination.sync="tpossTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn unelevated dense size="xs" icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.tpos" target="_blank"></q-btn>
</q-td>
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn flat dense size="xs" @click="deleteTPoS(props.row.id)" icon="cancel" color="pink"></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits TPoS extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "tpos/_api_docs.html" %}
<q-separator></q-separator>
{% include "tpos/_tpos.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="createTPoS" class="q-gutter-md">
<q-input filled dense
v-model.trim="formDialog.data.name"
label="Name"
placeholder="Tiago's PoS"></q-input>
<q-select filled dense
emit-value v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"></q-select>
<q-select filled dense
emit-value v-model="formDialog.data.currency"
:options="currencyOptions"
label="Currency *"></q-select>
<div class="row q-mt-lg">
<q-btn unelevated
color="deep-purple"
:disable="formDialog.data.currency == null || formDialog.data.name == null"
type="submit">Create TPoS</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.tpos"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteTPoS(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
{% endblock %}
{% block scripts %}
{{ window_vars(user) }}
<script>
var mapTPoS = function (obj) {
obj.date = Quasar.utils.date.formatDate(new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm');
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount);
obj.tpos = ['/tpos/', obj.id].join('');
return obj;
}
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits TPoS extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "tpos/_api_docs.html" %}
<q-separator></q-separator>
{% include "tpos/_tpos.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tposs: [],
currencyOptions: [
'USD','EUR','GBP','AED','AFN','ALL','AMD','ANG','AOA','ARS','AUD','AWG','AZN','BAM','BBD','BDT','BGN','BHD',
'BIF','BMD','BND','BOB','BRL','BSD','BTN','BWP','BYN','BZD','CAD','CDF','CHF','CLF','CLP','CNH','CNY','COP',
'CRC','CUC','CUP','CVE','CZK','DJF','DKK','DOP','DZD','EGP','ERN','ETB','EUR','FJD','FKP','GBP','GEL','GGP',
'GHS','GIP','GMD','GNF','GTQ','GYD','HKD','HNL','HRK','HTG','HUF','IDR','ILS','IMP','INR','IQD','IRR','ISK',
'JEP','JMD','JOD','JPY','KES','KGS','KHR','KMF','KPW','KRW','KWD','KYD','KZT','LAK','LBP','LKR','LRD','LSL',
'LYD','MAD','MDL','MGA','MKD','MMK','MNT','MOP','MRO','MUR','MVR','MWK','MXN','MYR','MZN','NAD','NGN','NIO','NOK','NPR','NZD','OMR','PAB','PEN','PGK','PHP','PKR','PLN','PYG','QAR','RON','RSD','RUB','RWF','SAR','SBD',
'SCR','SDG','SEK','SGD','SHP','SLL','SOS','SRD','SSP','STD','SVC','SYP','SZL','THB','TJS','TMT','TND','TOP',
'TRY','TTD','TWD','TZS','UAH','UGX','USD','UYU','UZS','VEF','VES','VND','VUV','WST','XAF','XAG','XAU','XCD',
'XDR','XOF','XPD','XPF','XPT','YER','ZAR','ZMW','ZWL'
],
tpossTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'currency', align: 'left', label: 'Currency', field: 'currency'}
],
pagination: {
rowsPerPage: 10
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px;">
<q-form @submit="createTPoS" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialog.data.name"
label="Name"
placeholder="Tiago's PoS"
></q-input>
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
<q-select
filled
dense
emit-value
v-model="formDialog.data.currency"
:options="currencyOptions"
label="Currency *"
></q-select>
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
:disable="formDialog.data.currency == null || formDialog.data.name == null"
type="submit"
>Create TPoS</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapTPoS = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.tpos = ['/tpos/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tposs: [],
currencyOptions: [
'USD',
'EUR',
'GBP',
'AED',
'AFN',
'ALL',
'AMD',
'ANG',
'AOA',
'ARS',
'AUD',
'AWG',
'AZN',
'BAM',
'BBD',
'BDT',
'BGN',
'BHD',
'BIF',
'BMD',
'BND',
'BOB',
'BRL',
'BSD',
'BTN',
'BWP',
'BYN',
'BZD',
'CAD',
'CDF',
'CHF',
'CLF',
'CLP',
'CNH',
'CNY',
'COP',
'CRC',
'CUC',
'CUP',
'CVE',
'CZK',
'DJF',
'DKK',
'DOP',
'DZD',
'EGP',
'ERN',
'ETB',
'EUR',
'FJD',
'FKP',
'GBP',
'GEL',
'GGP',
'GHS',
'GIP',
'GMD',
'GNF',
'GTQ',
'GYD',
'HKD',
'HNL',
'HRK',
'HTG',
'HUF',
'IDR',
'ILS',
'IMP',
'INR',
'IQD',
'IRR',
'ISK',
'JEP',
'JMD',
'JOD',
'JPY',
'KES',
'KGS',
'KHR',
'KMF',
'KPW',
'KRW',
'KWD',
'KYD',
'KZT',
'LAK',
'LBP',
'LKR',
'LRD',
'LSL',
'LYD',
'MAD',
'MDL',
'MGA',
'MKD',
'MMK',
'MNT',
'MOP',
'MRO',
'MUR',
'MVR',
'MWK',
'MXN',
'MYR',
'MZN',
'NAD',
'NGN',
'NIO',
'NOK',
'NPR',
'NZD',
'OMR',
'PAB',
'PEN',
'PGK',
'PHP',
'PKR',
'PLN',
'PYG',
'QAR',
'RON',
'RSD',
'RUB',
'RWF',
'SAR',
'SBD',
'SCR',
'SDG',
'SEK',
'SGD',
'SHP',
'SLL',
'SOS',
'SRD',
'SSP',
'STD',
'SVC',
'SYP',
'SZL',
'THB',
'TJS',
'TMT',
'TND',
'TOP',
'TRY',
'TTD',
'TWD',
'TZS',
'UAH',
'UGX',
'USD',
'UYU',
'UZS',
'VEF',
'VES',
'VND',
'VUV',
'WST',
'XAF',
'XAG',
'XAU',
'XCD',
'XDR',
'XOF',
'XPD',
'XPF',
'XPT',
'YER',
'ZAR',
'ZMW',
'ZWL'
],
tpossTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{
name: 'currency',
align: 'left',
label: 'Currency',
field: 'currency'
}
},
formDialog: {
show: false,
data: {}
],
pagination: {
rowsPerPage: 10
}
};
},
methods: {
closeFormDialog: function () {
this.formDialog.data = {};
},
getTPoSs: function () {
var self = this;
formDialog: {
show: false,
data: {}
}
}
},
methods: {
closeFormDialog: function () {
this.formDialog.data = {}
},
getTPoSs: function () {
var self = this
LNbits.api.request(
LNbits.api
.request(
'GET',
'/tpos/api/v1/tposs?all_wallets',
this.g.user.wallets[0].inkey
).then(function (response) {
)
.then(function (response) {
self.tposs = response.data.map(function (obj) {
return mapTPoS(obj);
});
});
},
createTPoS: function () {
var data = {
name: this.formDialog.data.name,
currency: this.formDialog.data.currency
};
var self = this;
return mapTPoS(obj)
})
})
},
createTPoS: function () {
var data = {
name: this.formDialog.data.name,
currency: this.formDialog.data.currency
}
var self = this
LNbits.api.request(
LNbits.api
.request(
'POST',
'/tpos/api/v1/tposs',
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet}).inkey,
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
.inkey,
data
).then(function (response) {
self.tposs.push(mapTPoS(response.data));
self.formDialog.show = false;
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
},
deleteTPoS: function (tposId) {
var self = this;
var tpos = _.findWhere(this.tposs, {id: tposId});
LNbits.utils.confirmDialog(
'Are you sure you want to delete this TPoS?'
).onOk(function () {
LNbits.api.request(
'DELETE',
'/tpos/api/v1/tposs/' + tposId,
_.findWhere(self.g.user.wallets, {id: tpos.wallet}).adminkey
).then(function (response) {
self.tposs = _.reject(self.tposs, function (obj) { return obj.id == tposId; });
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
});
},
exportCSV: function () {
LNbits.utils.exportCSV(this.tpossTable.columns, this.tposs);
}
)
.then(function (response) {
self.tposs.push(mapTPoS(response.data))
self.formDialog.show = false
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
created: function () {
if (this.g.user.wallets.length) {
this.getTPoSs();
}
deleteTPoS: function (tposId) {
var self = this
var tpos = _.findWhere(this.tposs, {id: tposId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this TPoS?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/tpos/api/v1/tposs/' + tposId,
_.findWhere(self.g.user.wallets, {id: tpos.wallet}).adminkey
)
.then(function (response) {
self.tposs = _.reject(self.tposs, function (obj) {
return obj.id == tposId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportCSV: function () {
LNbits.utils.exportCSV(this.tpossTable.columns, this.tposs)
}
});
</script>
},
created: function () {
if (this.g.user.wallets.length) {
this.getTPoSs()
}
}
})
</script>
{% endblock %}
+247 -207
View File
@@ -1,225 +1,265 @@
{% extends "public.html" %}
{% block toolbar_title %}{{ tpos.name }}{% endblock %}
{% block footer %}{% endblock %}
{% block page_container %}
<q-page-container>
<q-page>
<q-page-sticky v-if="exchangeRate" expand position="top">
<div class="row justify-center full-width">
<div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center">
<h3 class="q-mb-md">{% raw %}{{ famount }}{% endraw %}</h3>
<h5 class="q-mt-none">
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
</h5>
{% extends "public.html" %} {% block toolbar_title %}{{ tpos.name }}{% endblock
%} {% block footer %}{% endblock %} {% block page_container %}
<q-page-container>
<q-page>
<q-page-sticky v-if="exchangeRate" expand position="top">
<div class="row justify-center full-width">
<div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center">
<h3 class="q-mb-md">{% raw %}{{ famount }}{% endraw %}</h3>
<h5 class="q-mt-none">
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
</h5>
</div>
</div>
</q-page-sticky>
<q-page-sticky expand position="bottom">
<div class="row justify-center full-width">
<div class="col-12 col-sm-8 col-md-6 col-lg-4">
<div class="keypad q-pa-sm">
<q-btn unelevated @click="stack.push(1)" size="xl" color="grey-8"
>1</q-btn
>
<q-btn unelevated @click="stack.push(2)" size="xl" color="grey-8"
>2</q-btn
>
<q-btn unelevated @click="stack.push(3)" size="xl" color="grey-8"
>3</q-btn
>
<q-btn
unelevated
@click="stack = []"
size="xl"
color="pink"
class="btn-cancel"
>C</q-btn
>
<q-btn unelevated @click="stack.push(4)" size="xl" color="grey-8"
>4</q-btn
>
<q-btn unelevated @click="stack.push(5)" size="xl" color="grey-8"
>5</q-btn
>
<q-btn unelevated @click="stack.push(6)" size="xl" color="grey-8"
>6</q-btn
>
<q-btn unelevated @click="stack.push(7)" size="xl" color="grey-8"
>7</q-btn
>
<q-btn unelevated @click="stack.push(8)" size="xl" color="grey-8"
>8</q-btn
>
<q-btn unelevated @click="stack.push(9)" size="xl" color="grey-8"
>9</q-btn
>
<q-btn
unelevated
:disabled="amount == 0"
@click="showInvoice()"
size="xl"
color="green"
class="btn-confirm"
>OK</q-btn
>
<q-btn
unelevated
@click="stack.splice(-1, 1)"
size="xl"
color="grey-7"
>DEL</q-btn
>
<q-btn unelevated @click="stack.push(0)" size="xl" color="grey-8"
>0</q-btn
>
<q-btn
unelevated
@click="urlDialog.show = true"
size="xl"
color="grey-7"
>#</q-btn
>
</div>
</div>
</q-page-sticky>
<q-page-sticky expand position="bottom">
<div class="row justify-center full-width">
<div class="col-12 col-sm-8 col-md-6 col-lg-4">
<div class="keypad q-pa-sm">
<q-btn unelevated
@click="stack.push(1)"
size="xl" color="grey-8">1</q-btn>
<q-btn unelevated
@click="stack.push(2)"
size="xl" color="grey-8">2</q-btn>
<q-btn unelevated
@click="stack.push(3)"
size="xl" color="grey-8">3</q-btn>
<q-btn unelevated
@click="stack = []"
size="xl" color="pink" class="btn-cancel">C</q-btn>
<q-btn unelevated
@click="stack.push(4)"
size="xl" color="grey-8">4</q-btn>
<q-btn unelevated
@click="stack.push(5)"
size="xl" color="grey-8">5</q-btn>
<q-btn unelevated
@click="stack.push(6)"
size="xl" color="grey-8">6</q-btn>
<q-btn unelevated
@click="stack.push(7)"
size="xl" color="grey-8">7</q-btn>
<q-btn unelevated
@click="stack.push(8)"
size="xl" color="grey-8">8</q-btn>
<q-btn unelevated
@click="stack.push(9)"
size="xl" color="grey-8">9</q-btn>
<q-btn unelevated
:disabled="amount == 0"
@click="showInvoice()"
size="xl" color="green" class="btn-confirm">OK</q-btn>
<q-btn unelevated
@click="stack.splice(-1, 1)"
size="xl" color="grey-7">DEL</q-btn>
<q-btn unelevated
@click="stack.push(0)"
size="xl" color="grey-8">0</q-btn>
<q-btn unelevated
@click="urlDialog.show = true"
size="xl" color="grey-7">#</q-btn>
</div>
</div>
</div>
</q-page-sticky>
<q-dialog
v-model="invoiceDialog.show"
position="top"
@hide="closeInvoiceDialog"
>
<q-card
v-if="invoiceDialog.data"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
:value="invoiceDialog.data.payment_request"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
<div class="text-center">
<h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3>
<h5 class="q-mt-none">
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
</h5>
</div>
</q-page-sticky>
<q-dialog v-model="invoiceDialog.show" position="top" @hide="closeInvoiceDialog">
<q-card v-if="invoiceDialog.data" class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode :value="invoiceDialog.data.payment_request" :options="{width: 800}" class="rounded-borders"></qrcode>
</q-responsive>
<div class="text-center">
<h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3>
<h5 class="q-mt-none">
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
</h5>
</div>
<div class="row q-mt-lg">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="urlDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode value="{{ request.url }}" :options="{width: 800}" class="rounded-borders"></qrcode>
</q-responsive>
<div class="text-center q-mb-xl">
<p style="word-break: break-all"><strong>{{ tpos.name }}</strong><br>{{ request.url }}</p>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText('{{ request.url }}', 'TPoS URL copied to clipboard!')">Copy URL</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</q-page>
</q-page-container>
{% endblock %}
<div class="row q-mt-lg">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="urlDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
value="{{ request.url }}"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
<div class="text-center q-mb-xl">
<p style="word-break: break-all;">
<strong>{{ tpos.name }}</strong><br />{{ request.url }}
</p>
</div>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText('{{ request.url }}', 'TPoS URL copied to clipboard!')"
>Copy URL</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</q-page>
</q-page-container>
{% endblock %} {% block styles %}
<style>
.keypad {
display: grid;
grid-gap: 8px;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
}
.keypad .btn {
height: 100%;
}
.btn-cancel,
.btn-confirm {
grid-row: auto/span 2;
}
</style>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode)
{% block styles %}
<style>
.keypad {
display: grid;
grid-gap: 8px;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
}
.keypad .btn {
height: 100%;
}
.btn-cancel, .btn-confirm {
grid-row: auto/span 2;
}
</style>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode);
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tposId: '{{ tpos.id }}',
currency: '{{ tpos.currency }}',
exchangeRate: null,
stack: [],
invoiceDialog: {
show: false,
data: null,
dismissMsg: null,
paymentChecker: null
},
urlDialog: {
show: false
}
};
},
computed: {
amount: function () {
if (!this.stack.length) return 0.00;
return (Number(this.stack.join('')) / 100).toFixed(2);
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tposId: '{{ tpos.id }}',
currency: '{{ tpos.currency }}',
exchangeRate: null,
stack: [],
invoiceDialog: {
show: false,
data: null,
dismissMsg: null,
paymentChecker: null
},
famount: function () {
return LNbits.utils.formatCurrency(this.amount, this.currency);
},
sat: function () {
if (!this.exchangeRate) return 0;
return Math.ceil((this.amount / this.exchangeRate) * 100000000);
},
fsat: function () {
return LNbits.utils.formatSat(this.sat);
urlDialog: {
show: false
}
}
},
computed: {
amount: function () {
if (!this.stack.length) return 0.0
return (Number(this.stack.join('')) / 100).toFixed(2)
},
methods: {
closeInvoiceDialog: function () {
this.stack = [];
var dialog = this.invoiceDialog;
setTimeout(function () {
clearInterval(dialog.paymentChecker);
dialog.dismissMsg();
}, 3000);
},
showInvoice: function () {
var self = this;
var dialog = this.invoiceDialog;
famount: function () {
return LNbits.utils.formatCurrency(this.amount, this.currency)
},
sat: function () {
if (!this.exchangeRate) return 0
return Math.ceil((this.amount / this.exchangeRate) * 100000000)
},
fsat: function () {
return LNbits.utils.formatSat(this.sat)
}
},
methods: {
closeInvoiceDialog: function () {
this.stack = []
var dialog = this.invoiceDialog
setTimeout(function () {
clearInterval(dialog.paymentChecker)
dialog.dismissMsg()
}, 3000)
},
showInvoice: function () {
var self = this
var dialog = this.invoiceDialog
axios.post(
'/tpos/api/v1/tposs/' + this.tposId + '/invoices/',
{amount: this.sat}
).then(function (response) {
dialog.data = response.data;
dialog.show = true;
axios
.post('/tpos/api/v1/tposs/' + this.tposId + '/invoices/', {
amount: this.sat
})
.then(function (response) {
dialog.data = response.data
dialog.show = true
dialog.dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
});
})
dialog.paymentChecker = setInterval(function () {
axios.get(
'/tpos/api/v1/tposs/' + self.tposId + '/invoices/' + response.data.checking_id
).then(function (res) {
if (res.data.paid) {
clearInterval(dialog.paymentChecker);
dialog.dismissMsg();
dialog.show = false;
axios
.get(
'/tpos/api/v1/tposs/' +
self.tposId +
'/invoices/' +
response.data.checking_id
)
.then(function (res) {
if (res.data.paid) {
clearInterval(dialog.paymentChecker)
dialog.dismissMsg()
dialog.show = false
self.$q.notify({
type: 'positive',
message: self.fsat + ' sat received!',
icon: null
});
}
});
}, 2000);
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
},
getRates: function () {
var self = this;
axios.get("https://api.opennode.co/v1/rates").then(function (response) {
self.exchangeRate = response.data.data['BTC' + self.currency][self.currency];
});
}
self.$q.notify({
type: 'positive',
message: self.fsat + ' sat received!',
icon: null
})
}
})
}, 2000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
created: function () {
var getRates = this.getRates;
getRates();
setInterval(function () { getRates(); }, 20000);
getRates: function () {
var self = this
axios.get('https://api.opennode.co/v1/rates').then(function (response) {
self.exchangeRate =
response.data.data['BTC' + self.currency][self.currency]
})
}
});
</script>
},
created: function () {
var getRates = this.getRates
getRates()
setInterval(function () {
getRates()
}, 20000)
}
})
</script>
{% endblock %}
+2 -2
View File
@@ -1,7 +1,7 @@
from flask import g, abort, render_template
from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.helpers import Status
from lnbits.extensions.tpos import tpos_ext
from .crud import get_tpos
@@ -16,6 +16,6 @@ def index():
@tpos_ext.route("/<tpos_id>")
def tpos(tpos_id):
tpos = get_tpos(tpos_id) or abort(Status.NOT_FOUND, "TPoS does not exist.")
tpos = get_tpos(tpos_id) or abort(HTTPStatus.NOT_FOUND, "TPoS does not exist.")
return render_template("tpos/tpos.html", tpos=tpos)
+13 -13
View File
@@ -1,9 +1,9 @@
from flask import g, jsonify, request
from http import HTTPStatus
from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.helpers import Status
from lnbits.settings import WALLET
from lnbits.extensions.tpos import tpos_ext
@@ -18,7 +18,7 @@ def api_tposs():
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([tpos._asdict() for tpos in get_tposs(wallet_ids)]), Status.OK
return jsonify([tpos._asdict() for tpos in get_tposs(wallet_ids)]), HTTPStatus.OK
@tpos_ext.route("/api/v1/tposs", methods=["POST"])
@@ -32,7 +32,7 @@ def api_tposs():
def api_tpos_create():
tpos = create_tpos(wallet_id=g.wallet.id, **g.data)
return jsonify(tpos._asdict()), Status.CREATED
return jsonify(tpos._asdict()), HTTPStatus.CREATED
@tpos_ext.route("/api/v1/tposs/<tpos_id>", methods=["DELETE"])
@@ -41,14 +41,14 @@ def api_tpos_delete(tpos_id):
tpos = get_tpos(tpos_id)
if not tpos:
return jsonify({"message": "TPoS does not exist."}), Status.NOT_FOUND
return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND
if tpos.wallet != g.wallet.id:
return jsonify({"message": "Not your TPoS."}), Status.FORBIDDEN
return jsonify({"message": "Not your TPoS."}), HTTPStatus.FORBIDDEN
delete_tpos(tpos_id)
return "", Status.NO_CONTENT
return "", HTTPStatus.NO_CONTENT
@tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/", methods=["POST"])
@@ -57,16 +57,16 @@ def api_tpos_create_invoice(tpos_id):
tpos = get_tpos(tpos_id)
if not tpos:
return jsonify({"message": "TPoS does not exist."}), Status.NOT_FOUND
return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND
try:
checking_id, payment_request = create_invoice(
wallet_id=tpos.wallet, amount=g.data["amount"], memo=f"#tpos {tpos.name}"
)
except Exception as e:
return jsonify({"message": str(e)}), Status.INTERNAL_SERVER_ERROR
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), Status.CREATED
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED
@tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/<checking_id>", methods=["GET"])
@@ -74,18 +74,18 @@ def api_tpos_check_invoice(tpos_id, checking_id):
tpos = get_tpos(tpos_id)
if not tpos:
return jsonify({"message": "TPoS does not exist."}), Status.NOT_FOUND
return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND
try:
is_paid = not WALLET.get_invoice_status(checking_id).pending
except Exception:
return jsonify({"paid": False}), Status.OK
return jsonify({"paid": False}), HTTPStatus.OK
if is_paid:
wallet = get_wallet(tpos.wallet)
payment = wallet.get_payment(checking_id)
payment.set_pending(False)
return jsonify({"paid": True}), Status.OK
return jsonify({"paid": True}), HTTPStatus.OK
return jsonify({"paid": False}), Status.OK
return jsonify({"paid": False}), HTTPStatus.OK
+3
View File
@@ -0,0 +1,3 @@
<h1>User Manager</h1>
<h2>Make and manager users/wallets</h2>
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.
@@ -0,0 +1,8 @@
from flask import Blueprint
usermanager_ext: Blueprint = Blueprint("usermanager", __name__, static_folder="static", template_folder="templates")
from .views_api import * # noqa
from .views import * # noqa
@@ -0,0 +1,6 @@
{
"name": "User Manager",
"short_description": "Generate users and wallets",
"icon": "person_add",
"contributors": ["benarc"]
}
+117
View File
@@ -0,0 +1,117 @@
from lnbits.db import open_ext_db
from lnbits.settings import WALLET
from .models import Users, Wallets
from typing import List, Optional, Union
from ...core.crud import (
create_account,
get_user,
update_user_extension,
get_wallet_payments,
create_wallet,
delete_wallet,
)
###Users
def create_usermanager_user(user_name: str, wallet_name: str, admin_id: str) -> Users:
user = get_user(create_account().id)
wallet = create_wallet(user_id=user.id, wallet_name=wallet_name)
with open_ext_db("usermanager") as db:
db.execute(
"""
INSERT INTO users (id, name, admin)
VALUES (?, ?, ?)
""",
(user.id, user_name, admin_id),
)
db.execute(
"""
INSERT INTO wallets (id, admin, name, user, adminkey, inkey)
VALUES (?, ?, ?, ?, ?, ?)
""",
(wallet.id, admin_id, wallet_name, user.id, wallet.adminkey, wallet.inkey)
)
return get_usermanager_user(user.id)
def get_usermanager_user(user_id: str) -> Users:
with open_ext_db("usermanager") as db:
row = db.fetchone("SELECT * FROM users WHERE id = ?", (user_id,))
return Users(**row) if row else None
def get_usermanager_users(user_id: str) -> Users:
with open_ext_db("usermanager") as db:
rows = db.fetchall("SELECT * FROM users WHERE admin = ?", (user_id,))
return [Users(**row) for row in rows]
def delete_usermanager_user(user_id: str) -> None:
row = get_usermanager_wallets(user_id)
print("test")
with open_ext_db("usermanager") as db:
db.execute("DELETE FROM users WHERE id = ?", (user_id,))
row
for r in row:
delete_wallet( user_id=user_id, wallet_id=r.id)
with open_ext_db("usermanager") as dbb:
dbb.execute("DELETE FROM wallets WHERE user = ?", (user_id,))
###Wallets
def create_usermanager_wallet(user_id: str, wallet_name: str, admin_id: str) -> Wallets:
wallet = create_wallet(user_id=user_id, wallet_name=wallet_name)
with open_ext_db("usermanager") as db:
db.execute(
"""
INSERT INTO wallets (id, admin, name, user, adminkey, inkey)
VALUES (?, ?, ?, ?, ?, ?)
""",
(wallet.id, admin_id, wallet_name, user_id, wallet.adminkey, wallet.inkey)
)
return get_usermanager_wallet(wallet.id)
def get_usermanager_wallet(wallet_id: str) -> Optional[Wallets]:
with open_ext_db("usermanager") as db:
row = db.fetchone("SELECT * FROM wallets WHERE id = ?", (wallet_id,))
return Wallets(**row) if row else None
def get_usermanager_wallets(user_id: str) -> Wallets:
with open_ext_db("usermanager") as db:
rows = db.fetchall("SELECT * FROM wallets WHERE admin = ?", (user_id,))
return [Wallets(**row) for row in rows]
def get_usermanager_wallet_transactions(wallet_id: str) -> Users:
return get_wallet_payments(wallet_id=wallet_id,include_all_pending=False)
def get_usermanager_wallet_balances(user_id: str) -> Users:
user = get_user(user_id)
return (user.wallets)
def delete_usermanager_wallet(wallet_id: str, user_id: str) -> None:
delete_wallet( user_id=user_id, wallet_id=wallet_id)
with open_ext_db("usermanager") as db:
db.execute("DELETE FROM wallets WHERE id = ?", (wallet_id,))
@@ -0,0 +1,36 @@
from lnbits.db import open_ext_db
def m001_initial(db):
"""
Initial users table.
"""
db.execute("""
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
admin TEXT NOT NULL,
email TEXT,
password TEXT
);
""")
"""
Initial wallets table.
"""
db.execute("""
CREATE TABLE IF NOT EXISTS 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
);
""")
def migrate():
with open_ext_db("usermanager") as db:
m001_initial(db)
+18
View File
@@ -0,0 +1,18 @@
from typing import NamedTuple
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
@@ -0,0 +1,239 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">
User Manager: Make and manager users/wallets
</h5>
<p>
To help developers use LNbits to manage their users, the User Manager
extension allows the creation and management of users and wallets.
<br />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.<br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="GET users">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/usermanager/api/v1/users</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>JSON list of users</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}usermanager/api/v1/users -H
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET wallets">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/usermanager/api/v1/wallets/&lt;user_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>JSON wallet data</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root
}}usermanager/api/v1/wallets/&lt;user_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET transactions">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/usermanager/api/v1/wallets&lt;wallet_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>JSON a wallets transactions</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root
}}usermanager/api/v1/wallets&lt;wallet_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="POST user + initial wallet"
>
<q-card>
<q-card-section>
<code
><span class="text-light-green">POST</span>
/usermanager/api/v1/users</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code
>{"X-Api-Key": &lt;string&gt;, "Content-type":
"application/json"}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json) - "admin_id" is a YOUR user ID
</h5>
<code
>{"admin_id": &lt;string&gt;, "user_name": &lt;string&gt;,
"wallet_name": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"checking_id": &lt;string&gt;,"payment_request":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}usermanager/api/v1/users -d
'{"admin_id": "{{ g.user.id }}", "wallet_name": &lt;string&gt;,
"user_name": &lt;string&gt;}' -H "X-Api-Key: {{
g.user.wallets[0].inkey }}" -H "Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="POST wallet">
<q-card>
<q-card-section>
<code
><span class="text-light-green">POST</span>
/usermanager/api/v1/wallets</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code
>{"X-Api-Key": &lt;string&gt;, "Content-type":
"application/json"}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json) - "admin_id" is a YOUR user ID
</h5>
<code
>{"user_id": &lt;string&gt;, "wallet_name": &lt;string&gt;,
"admin_id": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"checking_id": &lt;string&gt;,"payment_request":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}usermanager/api/v1/wallets -d
'{"user_id": &lt;string&gt;, "wallet_name": &lt;string&gt;,
"admin_id": "{{ g.user.id }}"}' -H "X-Api-Key: {{
g.user.wallets[0].inkey }}" -H "Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="DELETE user and their wallets"
>
<q-card>
<q-card-section>
<code
><span class="text-red">DELETE</span>
/usermanager/api/v1/users/&lt;user_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.url_root
}}usermanager/api/v1/users/&lt;user_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="DELETE wallet">
<q-card>
<q-card-section>
<code
><span class="text-red">DELETE</span>
/usermanager/api/v1/wallets/&lt;wallet_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.url_root
}}usermanager/api/v1/wallets/&lt;wallet_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="POST activate extension"
>
<q-card>
<q-card-section>
<code
><span class="text-green">POST</span>
/usermanager/api/v1/extensions</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}usermanager/api/v1/extensions -d
'{"userid": &lt;string&gt;, "extension": &lt;string&gt;, "active":
&lt;integer&gt;}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
"Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
@@ -0,0 +1,453 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="userDialog.show = true"
>New User</q-btn
>
<q-btn unelevated color="deep-purple" @click="walletDialog.show = true"
>New Wallet
</q-btn>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Users</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportUsersCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="users"
row-key="id"
:columns="usersTable.columns"
:pagination.sync="usersTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteUser(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Wallets</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportWalletsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="wallets"
row-key="id"
:columns="walletsTable.columns"
:pagination.sync="walletsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="account_balance_wallet"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.walllink"
target="_blank"
></q-btn>
<q-tooltip>
Link to wallet
</q-tooltip>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteWallet(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits User Manager Extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "usermanager/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="userDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px;">
<q-form @submit="sendUserFormData" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="userDialog.data.usrname"
label="Username"
></q-input>
<q-input
filled
dense
v-model.trim="userDialog.data.walname"
label="Initial wallet name"
></q-input>
<q-btn
unelevated
color="deep-purple"
:disable="userDialog.data.walname == null"
type="submit"
>Create User</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="walletDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px;">
<q-form @submit="sendWalletFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="walletDialog.data.user"
:options="userOptions"
label="User *"
>
</q-select>
<q-input
filled
dense
v-model.trim="walletDialog.data.walname"
label="Wallet name"
></q-input>
<q-btn
unelevated
color="deep-purple"
:disable="walletDialog.data.walname == null"
type="submit"
>Create Wallet</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapUserManager = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.walllink = ['../wallet?usr=', obj.user, '&wal=', obj.id].join('')
obj._data = _.clone(obj)
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
wallets: [],
users: [],
usersTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Username', field: 'name'}
],
pagination: {
rowsPerPage: 10
}
},
walletsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'user', align: 'left', label: 'User', field: 'user'},
{
name: 'adminkey',
align: 'left',
label: 'Admin Key',
field: 'adminkey'
},
{name: 'inkey', align: 'left', label: 'Invoice Key', field: 'inkey'}
],
pagination: {
rowsPerPage: 10
}
},
walletDialog: {
show: false,
data: {}
},
userDialog: {
show: false,
data: {}
}
}
},
computed: {
userOptions: function () {
return this.users.map(function (obj) {
console.log(obj.id)
return {
value: String(obj.id),
label: String(obj.id)
}
})
}
},
methods: {
///////////////Users////////////////////////////
getUsers: function () {
var self = this
LNbits.api
.request(
'GET',
'/usermanager/api/v1/users',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.users = response.data.map(function (obj) {
return mapUserManager(obj)
})
})
},
openUserUpdateDialog: function (linkId) {
var link = _.findWhere(this.users, {id: linkId})
this.userDialog.data = _.clone(link._data)
this.userDialog.show = true
},
sendUserFormData: function () {
if (this.userDialog.data.id) {
} else {
var data = {
admin_id: this.g.user.id,
user_name: this.userDialog.data.usrname,
wallet_name: this.userDialog.data.walname
}
}
{
this.createUser(data)
}
},
createUser: function (data) {
var self = this
LNbits.api
.request(
'POST',
'/usermanager/api/v1/users',
this.g.user.wallets[0].inkey,
data
)
.then(function (response) {
self.users.push(mapUserManager(response.data))
self.userDialog.show = false
self.userDialog.data = {}
data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteUser: function (userId) {
var self = this
console.log(userId)
LNbits.utils
.confirmDialog('Are you sure you want to delete this User link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/usermanager/api/v1/users/' + userId,
self.g.user.wallets[0].inkey
)
.then(function (response) {
self.users = _.reject(self.users, function (obj) {
return obj.id == userId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportUsersCSV: function () {
LNbits.utils.exportCSV(this.usersTable.columns, this.users)
},
///////////////Wallets////////////////////////////
getWallets: function () {
var self = this
LNbits.api
.request(
'GET',
'/usermanager/api/v1/wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.wallets = response.data.map(function (obj) {
return mapUserManager(obj)
})
})
},
openWalletUpdateDialog: function (linkId) {
var link = _.findWhere(this.users, {id: linkId})
this.walletDialog.data = _.clone(link._data)
this.walletDialog.show = true
},
sendWalletFormData: function () {
if (this.walletDialog.data.id) {
} else {
var data = {
user_id: this.walletDialog.data.user,
admin_id: this.g.user.id,
wallet_name: this.walletDialog.data.walname
}
}
{
this.createWallet(data)
}
},
createWallet: function (data) {
var self = this
LNbits.api
.request(
'POST',
'/usermanager/api/v1/wallets',
this.g.user.wallets[0].inkey,
data
)
.then(function (response) {
self.wallets.push(mapUserManager(response.data))
self.walletDialog.show = false
self.walletDialog.data = {}
data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteWallet: function (userId) {
var self = this
LNbits.utils
.confirmDialog('Are you sure you want to delete this wallet link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/usermanager/api/v1/wallets/' + userId,
self.g.user.wallets[0].inkey
)
.then(function (response) {
self.wallets = _.reject(self.wallets, function (obj) {
return obj.id == userId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportWalletsCSV: function () {
LNbits.utils.exportCSV(this.walletsTable.columns, this.wallets)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getUsers()
this.getWallets()
}
}
})
</script>
{% endblock %}
+13
View File
@@ -0,0 +1,13 @@
from flask import g, abort, render_template, jsonify
import json
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.usermanager import usermanager_ext
from lnbits.db import open_ext_db
@usermanager_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
def index():
return render_template("usermanager/index.html", user=g.user)

Some files were not shown because too many files have changed in this diff Show More