Merge branch 'master' into jukebox/addlistener

This commit is contained in:
Arc 2021-07-01 00:42:34 +01:00 committed by GitHub
commit 7c56501225
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 3109 additions and 684 deletions

View File

@ -5,7 +5,7 @@ QUART_DEBUG=true
HOST=127.0.0.1
PORT=5000
LNBITS_SITE_TITLE=LNbits
LNBITS_ALLOWED_USERS=""
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_DATA_FOLDER="./data"
@ -13,6 +13,11 @@ LNBITS_DISABLED_EXTENSIONS="amilk"
LNBITS_FORCE_HTTPS=true
LNBITS_SERVICE_FEE="0.0"
# Change theme
LNBITS_SITE_TITLE=LNbits
# Choose from mint, flamingo, quasar, autumn, monochrome
LNBITS_THEME_OPTIONS="mint, flamingo, quasar, autumn, monochrome, salvador"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC),
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet
LNBITS_BACKEND_WALLET_CLASS=VoidWallet

View File

@ -17,7 +17,6 @@ shortuuid = "*"
quart = "*"
quart-cors = "*"
quart-compress = "*"
secure = "*"
typing-extensions = "*"
httpx = "*"
quart-trio = "*"
@ -35,3 +34,4 @@ pytest = "*"
pytest-cov = "*"
mypy = "latest"
pytest-trio = "*"
trio-typing = "*"

443
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "e12af74353e8bea3f97bf2aea16a1ba0a6e4c3a08042ce7368187a06e7791e2c"
"sha256": "8c4056a80c682fac834266c11892573ce53807226c0810e4564976656ea5ff45"
},
"pipfile-spec": 6,
"requires": {
@ -18,10 +18,19 @@
"default": {
"aiofiles": {
"hashes": [
"sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27",
"sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"
"sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4",
"sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"
],
"version": "==0.6.0"
"markers": "python_version >= '3.6' and python_version < '4.0'",
"version": "==0.7.0"
},
"anyio": {
"hashes": [
"sha256:41c4be842c284222b197a625d76a7ab85adf9d52788f563172fe180c2744b6c1",
"sha256:89e19b1498c8a6f12277e0bd2949597e445aa1b14361fbab2c36943639ef5190"
],
"markers": "python_full_version >= '3.6.2'",
"version": "==3.2.0"
},
"async-generator": {
"hashes": [
@ -33,11 +42,11 @@
},
"attrs": {
"hashes": [
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.3.0"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.2.0"
},
"bech32": {
"hashes": [
@ -99,41 +108,40 @@
},
"cerberus": {
"hashes": [
"sha256:7aff49bc793e58a88ac14bffc3eca0f67e077881d3c62c621679a621294dd174",
"sha256:eec10585c33044fb7c69650bc5b68018dac0443753337e2b07684ee0f3c83329"
"sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"
],
"index": "pypi",
"version": "==1.3.3"
"version": "==1.3.4"
},
"certifi": {
"hashes": [
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
],
"version": "==2020.12.5"
"version": "==2021.5.30"
},
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
"sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a",
"sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
"markers": "python_version >= '3.6'",
"version": "==8.0.1"
},
"ecdsa": {
"hashes": [
"sha256:881fa5e12bb992972d3d1b3d4dfbe149ab76a89f13da02daa5ea1ec7dea6e747",
"sha256:cfc046a2ddd425adbd1a78b3c46f0d1325c657811c0f45ecc3a0a6236c1e50ff"
"sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676",
"sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"
],
"index": "pypi",
"version": "==0.16.1"
"version": "==0.17.0"
},
"embit": {
"hashes": [
"sha256:7c4264d7ede8e2c114db10585270874c9df809c68d2e21db918872e3245b5f2b"
"sha256:d67fc0f7fbdb7588c3eb24441bf8e05770056260bc8e5537399a1b3ce5ccf12a"
],
"index": "pypi",
"version": "==0.2.1"
"version": "==0.4.2"
},
"environs": {
"hashes": [
@ -169,19 +177,19 @@
},
"httpcore": {
"hashes": [
"sha256:37ae835fb370049b2030c3290e12ed298bf1473c41bb72ca4aa78681eba9b7c9",
"sha256:93e822cd16c32016b414b789aeff4e855d0ccbfc51df563ee34d4dbadbb3bcdc"
"sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e",
"sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff"
],
"markers": "python_version >= '3.6'",
"version": "==0.12.3"
"version": "==0.13.6"
},
"httpx": {
"hashes": [
"sha256:cc2a55188e4b25272d2bcd46379d300f632045de4377682aa98a8a6069d55967",
"sha256:d379653bd457e8257eb0df99cb94557e4aac441b7ba948e333be969298cac272"
"sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c",
"sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6"
],
"index": "pypi",
"version": "==0.17.1"
"version": "==0.18.2"
},
"hypercorn": {
"extras": [
@ -196,34 +204,34 @@
},
"hyperframe": {
"hashes": [
"sha256:742d2a4bc3152a340a49d59f32e33ec420aa8e7054c1444ef5c7efff255842f1",
"sha256:a51026b1591cac726fc3d0b7994fbc7dc5efab861ef38503face2930fd7b2d34"
"sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15",
"sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==6.0.0"
"version": "==6.0.1"
},
"idna": {
"hashes": [
"sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16",
"sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1"
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
],
"version": "==3.1"
"version": "==3.2"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
"sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c",
"sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.0"
"markers": "python_version >= '3.6'",
"version": "==2.0.1"
},
"jinja2": {
"hashes": [
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419",
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"
"sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4",
"sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.11.3"
"markers": "python_version >= '3.6'",
"version": "==3.0.1"
},
"lnurl": {
"hashes": [
@ -235,69 +243,87 @@
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f",
"sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014",
"sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85",
"sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850",
"sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1",
"sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5",
"sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c",
"sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
"sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
"sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
"sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
"sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
"sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
"sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
"sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
"sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
"sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
"sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
"sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
"sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
"sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
"sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
"sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
"sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
"sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
"sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
"sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
"sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
"sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
"sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
"sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
"sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
"sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
"sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
"sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
"sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
"sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
"sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
"sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
"sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
"sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
"sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1"
"markers": "python_version >= '3.6'",
"version": "==2.0.1"
},
"marshmallow": {
"hashes": [
"sha256:0dd42891a5ef288217ed6410917f3c6048f585f8692075a0052c24f9bfff9dfd",
"sha256:16e99cb7f630c0ef4d7d364ed0109ac194268dde123966076ab3dafb9ae3906b"
"sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040",
"sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01"
],
"markers": "python_version >= '3.5'",
"version": "==3.11.1"
"version": "==3.12.1"
},
"mypy": {
"hashes": [
"sha256:0190fb77e93ce971954c9e54ea61de2802065174e5e990c9d4c1d0f54fbeeca2",
"sha256:0756529da2dd4d53d26096b7969ce0a47997123261a5432b48cc6848a2cb0bd4",
"sha256:2f9fedc1f186697fda191e634ac1d02f03d4c260212ccb018fabbb6d4b03eee8",
"sha256:353aac2ce41ddeaf7599f1c73fed2b75750bef3b44b6ad12985a991bc002a0da",
"sha256:3f12705eabdd274b98f676e3e5a89f247ea86dc1af48a2d5a2b080abac4e1243",
"sha256:4efc67b9b3e2fddbe395700f91d5b8deb5980bfaaccb77b306310bd0b9e002eb",
"sha256:517e7528d1be7e187a5db7f0a3e479747307c1b897d9706b1c662014faba3116",
"sha256:68a098c104ae2b75e946b107ef69dd8398d54cb52ad57580dfb9fc78f7f997f0",
"sha256:746e0b0101b8efec34902810047f26a8c80e1efbb4fc554956d848c05ef85d76",
"sha256:8be7bbd091886bde9fcafed8dd089a766fa76eb223135fe5c9e9798f78023a20",
"sha256:9236c21194fde5df1b4d8ebc2ef2c1f2a5dc7f18bcbea54274937cae2e20a01c",
"sha256:9ef5355eaaf7a23ab157c21a44c614365238a7bdb3552ec3b80c393697d974e1",
"sha256:9f1d74eeb3f58c7bd3f3f92b8f63cb1678466a55e2c4612bf36909105d0724ab",
"sha256:a26d0e53e90815c765f91966442775cf03b8a7514a4e960de7b5320208b07269",
"sha256:ae94c31bb556ddb2310e4f913b706696ccbd43c62d3331cd3511caef466871d2",
"sha256:b5ba1f0d5f9087e03bf5958c28d421a03a4c1ad260bf81556195dffeccd979c4",
"sha256:b5dfcd22c6bab08dfeded8d5b44bdcb68c6f1ab261861e35c470b89074f78a70",
"sha256:cd01c599cf9f897b6b6c6b5d8b182557fb7d99326bcdf5d449a0fbbb4ccee4b9",
"sha256:e89880168c67cf4fde4506b80ee42f1537ad66ad366c101d388b3fd7d7ce2afd",
"sha256:ebe2bc9cb638475f5d39068d2dbe8ae1d605bb8d8d3ff281c695df1670ab3987",
"sha256:f89bfda7f0f66b789792ab64ce0978e4a991a0e4dd6197349d0767b0f1095b21",
"sha256:fc4d63da57ef0e8cd4ab45131f3fe5c286ce7dd7f032650d0fbc239c6190e167",
"sha256:fd634bc17b1e2d6ce716f0e43446d0d61cdadb1efcad5c56ca211c22b246ebc8"
],
"markers": "implementation_name == 'cpython'",
"version": "==0.902"
},
"mypy-extensions": {
"hashes": [
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
],
"version": "==0.4.3"
},
"outcome": {
"hashes": [
@ -316,31 +342,31 @@
},
"pydantic": {
"hashes": [
"sha256:0c40162796fc8d0aa744875b60e4dc36834db9f2a25dbf9ba9664b1915a23850",
"sha256:20d42f1be7c7acc352b3d09b0cf505a9fab9deb93125061b376fbe1f06a5459f",
"sha256:2287ebff0018eec3cc69b1d09d4b7cebf277726fa1bd96b45806283c1d808683",
"sha256:258576f2d997ee4573469633592e8b99aa13bda182fcc28e875f866016c8e07e",
"sha256:26cf3cb2e68ec6c0cfcb6293e69fb3450c5fd1ace87f46b64f678b0d29eac4c3",
"sha256:2f2736d9a996b976cfdfe52455ad27462308c9d3d0ae21a2aa8b4cd1a78f47b9",
"sha256:3114d74329873af0a0e8004627f5389f3bb27f956b965ddd3e355fe984a1789c",
"sha256:3bbd023c981cbe26e6e21c8d2ce78485f85c2e77f7bab5ec15b7d2a1f491918f",
"sha256:3bcb9d7e1f9849a6bdbd027aabb3a06414abd6068cb3b21c49427956cce5038a",
"sha256:4bbc47cf7925c86a345d03b07086696ed916c7663cb76aa409edaa54546e53e2",
"sha256:6388ef4ef1435364c8cc9a8192238aed030595e873d8462447ccef2e17387125",
"sha256:830ef1a148012b640186bf4d9789a206c56071ff38f2460a32ae67ca21880eb8",
"sha256:8fbb677e4e89c8ab3d450df7b1d9caed23f254072e8597c33279460eeae59b99",
"sha256:c17a0b35c854049e67c68b48d55e026c84f35593c66d69b278b8b49e2484346f",
"sha256:dd4888b300769ecec194ca8f2699415f5f7760365ddbe243d4fd6581485fa5f0",
"sha256:dde4ca368e82791de97c2ec019681ffb437728090c0ff0c3852708cf923e0c7d",
"sha256:e3f8790c47ac42549dc8b045a67b0ca371c7f66e73040d0197ce6172b385e520",
"sha256:e8bc082afef97c5fd3903d05c6f7bb3a6af9fc18631b4cc9fedeb4720efb0c58",
"sha256:eb8ccf12295113ce0de38f80b25f736d62f0a8d87c6b88aca645f168f9c78771",
"sha256:fb77f7a7e111db1832ae3f8f44203691e15b1fa7e5a1cb9691d4e2659aee41c4",
"sha256:fbfb608febde1afd4743c6822c19060a8dbdd3eb30f98e36061ba4973308059e",
"sha256:fff29fe54ec419338c522b908154a2efabeee4f483e48990f87e189661f31ce3"
"sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd",
"sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739",
"sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f",
"sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840",
"sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23",
"sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287",
"sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62",
"sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b",
"sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb",
"sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820",
"sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3",
"sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b",
"sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e",
"sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3",
"sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316",
"sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b",
"sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4",
"sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20",
"sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e",
"sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505",
"sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1",
"sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==1.8.1"
"version": "==1.8.2"
},
"pypng": {
"hashes": [
@ -366,18 +392,18 @@
},
"python-dotenv": {
"hashes": [
"sha256:471b782da0af10da1a80341e8438fca5fadeba2881c54360d5fd8d03d03a4f4a",
"sha256:49782a97c9d641e8a09ae1d9af0856cc587c8d2474919342d5104d85be9890b2"
"sha256:dd8fe852847f4fbfadabf6183ddd4c824a9651f02d51714fa075c95561959c7d",
"sha256:effaac3c1e58d89b3ccb4d04a40dc7ad6e0275fda25fd75ae9d323e2465e202d"
],
"version": "==0.17.0"
"version": "==0.18.0"
},
"quart": {
"hashes": [
"sha256:429c5b4ff27e1d2f9ca0aacc38f6aba0ff49b38b815448bf24b613d3de12ea02",
"sha256:7b13786e07541cc9ce1466fdc6a6ccd5f36eb39118edd25a42d617593cd17707"
"sha256:f35134fb1d81af61624e6d89bca33cd611dcedce2dc4e291f527ab04395f4e1a",
"sha256:f80c91d1e0588662483e22dd9c368a5778886b62e128c5399d2cc1b1898482cf"
],
"index": "pypi",
"version": "==0.14.1"
"version": "==0.15.1"
},
"quart-compress": {
"hashes": [
@ -389,19 +415,19 @@
},
"quart-cors": {
"hashes": [
"sha256:0ea23ea8db2c21835f6698b91a09d99ab59f98f8d90a2a739475ef0409591573",
"sha256:e526e9929934ad31301853efe357a3bd2e08c3282aff37184fa8671ed854f052"
"sha256:c2be932f20413a56b176527090229afe8f725a3ee029d45ea08a174cdc319823",
"sha256:ea08d26aef918d59194fbf065cde9b6cae90dc5f21120dcd254d7d46190cd293"
],
"index": "pypi",
"version": "==0.4.0"
"version": "==0.5.0"
},
"quart-trio": {
"hashes": [
"sha256:1e7fce0df41afc3038bf0431b20614f90984de50341b19f9d4d3b9ba1ac7574a",
"sha256:933e3c18e232ece30ccbac7579fdc5f62f2f9c79c3273d6c341f5a1686791eb1"
"sha256:27617f0c9fa8759d3056e9ddcdc038d44093af45eb5f84f8d5714872aaaa8c7d",
"sha256:30dfab5e382f06c605d4a5960e8188e8e05d10198f02097f0a16c1dca41b3574"
],
"index": "pypi",
"version": "==0.7.0"
"version": "==0.8.0"
},
"represent": {
"hashes": [
@ -416,18 +442,18 @@
"idna2008"
],
"hashes": [
"sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d",
"sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
],
"version": "==1.4.0"
"version": "==1.5.0"
},
"secure": {
"hashes": [
"sha256:4dc8dd4b548831c3ad7f94079332c41d67c781eccc32215ff5a8a49582c1a447",
"sha256:b3bf1e39ebf40040fc3248392343a5052aa14cb45fc87ec91b0bd11f19cc46bd"
"sha256:6e30939d8f95bf3b8effb8a36ebb5ed57f265daeeae905e3aa9677ea538ab64e",
"sha256:a93b720c7614809c131ca80e477263140107c6c212829d0a6e1f7bc8d859c608"
],
"index": "pypi",
"version": "==0.2.1"
"version": "==0.3.0"
},
"shortuuid": {
"hashes": [
@ -439,11 +465,11 @@
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
"version": "==1.16.0"
},
"sniffio": {
"hashes": [
@ -455,10 +481,10 @@
},
"sortedcontainers": {
"hashes": [
"sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f",
"sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1"
"sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88",
"sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"
],
"version": "==2.3.0"
"version": "==2.4.0"
},
"sqlalchemy": {
"hashes": [
@ -528,22 +554,30 @@
"index": "pypi",
"version": "==0.16.0"
},
"typing-extensions": {
"trio-typing": {
"hashes": [
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
"sha256:35f1bec8df2150feab6c8b073b54135321722c9d9289bbffa78a9a091ea83b72",
"sha256:f2007df617a6c26a2294db0dd63645b5451149757e1bde4cb8dbf3e1369174fb"
],
"index": "pypi",
"version": "==3.7.4.3"
"version": "==0.5.0"
},
"typing-extensions": {
"hashes": [
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
],
"index": "pypi",
"version": "==3.10.0.0"
},
"werkzeug": {
"hashes": [
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
"sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42",
"sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.0.1"
"markers": "python_version >= '3.6'",
"version": "==2.0.1"
},
"wsproto": {
"hashes": [
@ -572,11 +606,11 @@
},
"attrs": {
"hashes": [
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.3.0"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.2.0"
},
"black": {
"hashes": [
@ -587,11 +621,11 @@
},
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
"sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a",
"sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
"markers": "python_version >= '3.6'",
"version": "==8.0.1"
},
"coverage": {
"hashes": [
@ -653,10 +687,10 @@
},
"idna": {
"hashes": [
"sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16",
"sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1"
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
],
"version": "==3.1"
"version": "==3.2"
},
"iniconfig": {
"hashes": [
@ -667,31 +701,32 @@
},
"mypy": {
"hashes": [
"sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e",
"sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064",
"sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c",
"sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4",
"sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97",
"sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df",
"sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8",
"sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a",
"sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56",
"sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7",
"sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6",
"sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5",
"sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a",
"sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521",
"sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564",
"sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49",
"sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66",
"sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a",
"sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119",
"sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506",
"sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c",
"sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb"
"sha256:0190fb77e93ce971954c9e54ea61de2802065174e5e990c9d4c1d0f54fbeeca2",
"sha256:0756529da2dd4d53d26096b7969ce0a47997123261a5432b48cc6848a2cb0bd4",
"sha256:2f9fedc1f186697fda191e634ac1d02f03d4c260212ccb018fabbb6d4b03eee8",
"sha256:353aac2ce41ddeaf7599f1c73fed2b75750bef3b44b6ad12985a991bc002a0da",
"sha256:3f12705eabdd274b98f676e3e5a89f247ea86dc1af48a2d5a2b080abac4e1243",
"sha256:4efc67b9b3e2fddbe395700f91d5b8deb5980bfaaccb77b306310bd0b9e002eb",
"sha256:517e7528d1be7e187a5db7f0a3e479747307c1b897d9706b1c662014faba3116",
"sha256:68a098c104ae2b75e946b107ef69dd8398d54cb52ad57580dfb9fc78f7f997f0",
"sha256:746e0b0101b8efec34902810047f26a8c80e1efbb4fc554956d848c05ef85d76",
"sha256:8be7bbd091886bde9fcafed8dd089a766fa76eb223135fe5c9e9798f78023a20",
"sha256:9236c21194fde5df1b4d8ebc2ef2c1f2a5dc7f18bcbea54274937cae2e20a01c",
"sha256:9ef5355eaaf7a23ab157c21a44c614365238a7bdb3552ec3b80c393697d974e1",
"sha256:9f1d74eeb3f58c7bd3f3f92b8f63cb1678466a55e2c4612bf36909105d0724ab",
"sha256:a26d0e53e90815c765f91966442775cf03b8a7514a4e960de7b5320208b07269",
"sha256:ae94c31bb556ddb2310e4f913b706696ccbd43c62d3331cd3511caef466871d2",
"sha256:b5ba1f0d5f9087e03bf5958c28d421a03a4c1ad260bf81556195dffeccd979c4",
"sha256:b5dfcd22c6bab08dfeded8d5b44bdcb68c6f1ab261861e35c470b89074f78a70",
"sha256:cd01c599cf9f897b6b6c6b5d8b182557fb7d99326bcdf5d449a0fbbb4ccee4b9",
"sha256:e89880168c67cf4fde4506b80ee42f1537ad66ad366c101d388b3fd7d7ce2afd",
"sha256:ebe2bc9cb638475f5d39068d2dbe8ae1d605bb8d8d3ff281c695df1670ab3987",
"sha256:f89bfda7f0f66b789792ab64ce0978e4a991a0e4dd6197349d0767b0f1095b21",
"sha256:fc4d63da57ef0e8cd4ab45131f3fe5c286ce7dd7f032650d0fbc239c6190e167",
"sha256:fd634bc17b1e2d6ce716f0e43446d0d61cdadb1efcad5c56ca211c22b246ebc8"
],
"index": "pypi",
"version": "==0.812"
"markers": "implementation_name == 'cpython'",
"version": "==0.902"
},
"mypy-extensions": {
"hashes": [
@ -749,19 +784,19 @@
},
"pytest": {
"hashes": [
"sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634",
"sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"
"sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
"sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
],
"index": "pypi",
"version": "==6.2.3"
"version": "==6.2.4"
},
"pytest-cov": {
"hashes": [
"sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7",
"sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"
"sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a",
"sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"
],
"index": "pypi",
"version": "==2.11.1"
"version": "==2.12.1"
},
"pytest-trio": {
"hashes": [
@ -826,10 +861,10 @@
},
"sortedcontainers": {
"hashes": [
"sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f",
"sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1"
"sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88",
"sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"
],
"version": "==2.3.0"
"version": "==2.4.0"
},
"toml": {
"hashes": [
@ -884,12 +919,12 @@
},
"typing-extensions": {
"hashes": [
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
],
"index": "pypi",
"version": "==3.7.4.3"
"version": "==3.10.0.0"
}
}
}

View File

@ -40,6 +40,13 @@ Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Ne
## Running the server
LNbits uses [Quart][quart] as an application server.
Before running the server for the first time, make sure to create the data folder:
```sh
$ mkdir data
```
To then run the server, use:
```sh
$ pipenv run python -m lnbits

View File

@ -23,11 +23,11 @@ mkdir data
./venv/bin/hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()'
```
No you can visit your LNbits at http://localhost:5000/.
Now you can visit your LNbits at http://localhost:5000/.
Now modify the `.env` file with any settings you prefer and add a proper [funding source](./wallets.md) by modifying the value of `LNBITS_BACKEND_WALLET_CLASS` and providing the extra information and credentials related to the chosen funding source.
Then you can run restart it and it will be using the new settings.
Then you can restart it and it will be using the new settings.
You might also need to install additional packages or perform additional setup steps, depending on the chosen backend. See [the short guide](./wallets.md) on each different funding source.
@ -37,7 +37,7 @@ Docker installation
To install using docker you first need to build the docker image as:
```
git clone https://github.com/lnbits/lnbits.git
cd lnbits/ # ${PWD} refered as <lnbits_repo>
cd lnbits/ # ${PWD} referred as <lnbits_repo>
docker build -t lnbits .
```
@ -56,5 +56,5 @@ sudo chown 1000:1000 ./data/
Then the image can be run as:
```
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits
``
Finally you can access the lnbits on your machine port 5000.
```
Finally you can access your lnbits on your machine at port 5000.

View File

@ -1,4 +1,4 @@
import trio # type: ignore
import trio
from .commands import migrate_databases, transpile_scss, bundle_vendored

View File

@ -7,7 +7,6 @@ from quart import g
from quart_trio import QuartTrio
from quart_cors import cors # type: ignore
from quart_compress import Compress # type: ignore
from secure import SecureHeaders # type: ignore
from .commands import db_migrate, handle_assets
from .core import core_app
@ -27,8 +26,6 @@ from .tasks import (
)
from .settings import WALLET
secure_headers = SecureHeaders(hsts=False, xfo=False)
def create_app(config_object="lnbits.settings") -> QuartTrio:
"""Create application factory.
@ -46,7 +43,6 @@ def create_app(config_object="lnbits.settings") -> QuartTrio:
register_blueprints(app)
register_filters(app)
register_commands(app)
register_request_hooks(app)
register_async_tasks(app)
register_exception_handlers(app)
@ -108,19 +104,11 @@ def register_assets(app: QuartTrio):
def register_filters(app: QuartTrio):
"""Jinja filters."""
app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"]
app.jinja_env.globals["LNBITS_THEME_OPTIONS"] = app.config["LNBITS_THEME_OPTIONS"]
app.jinja_env.globals["LNBITS_VERSION"] = app.config["LNBITS_COMMIT"]
app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions()
def register_request_hooks(app: QuartTrio):
"""Open the core db for each request so everything happens in a big transaction"""
@app.after_request
async def set_secure_headers(response):
secure_headers.quart(response)
return response
def register_async_tasks(app):
@app.route("/wallet/webhook", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
async def webhook_listener():

View File

@ -1,4 +1,4 @@
import trio # type: ignore
import trio
import warnings
import click
import importlib

View File

@ -1,4 +1,4 @@
import trio # type: ignore
import trio
import json
import httpx
from io import BytesIO
@ -229,10 +229,10 @@ async def redeem_lnurl_withdraw(
pass
async with httpx.AsyncClient() as client:
await client.get(
res["callback"],
params=params,
)
try:
await client.get(res["callback"], params=params)
except Exception:
pass
async def perform_lnurlauth(

View File

@ -119,6 +119,8 @@ new Vue({
paymentHash: null,
minMax: [0, 2100000000000000],
lnurl: null,
units: ['sat'],
unit: 'sat',
data: {
amount: null,
memo: ''
@ -233,6 +235,7 @@ new Vue({
this.receive.paymentHash = null
this.receive.data.amount = null
this.receive.data.memo = null
this.receive.unit = 'sat'
this.receive.paymentChecker = null
this.receive.minMax = [0, 2100000000000000]
this.receive.lnurl = null
@ -269,11 +272,13 @@ new Vue({
},
createInvoice: function () {
this.receive.status = 'loading'
LNbits.api
.createInvoice(
this.g.wallet,
this.receive.data.amount,
this.receive.data.memo,
this.receive.unit,
this.receive.lnurl && this.receive.lnurl.callback
)
.then(response => {
@ -619,6 +624,15 @@ new Vue({
created: function () {
this.fetchBalance()
this.fetchPayments()
LNbits.api
.request('GET', '/api/v1/currencies')
.then(response => {
this.receive.units = ['sat', ...response.data]
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
},
mounted: function () {
// show disclaimer

View File

@ -1,4 +1,4 @@
import trio # type: ignore
import trio
import httpx
from typing import List

View File

@ -17,14 +17,14 @@
></q-icon>
{% raw %}
<h5 class="q-mt-lg q-mb-xs">{{ extension.name }}</h5>
{{ extension.shortDescription }} {% endraw %}
<small>{{ extension.shortDescription }} </small>{% endraw %}
</q-card-section>
<q-separator></q-separator>
<q-card-actions>
<div v-if="extension.isEnabled">
<q-btn
flat
color="deep-purple"
color="primary"
type="a"
:href="[extension.url, '?usr=', g.user.id].join('')"
>Open</q-btn
@ -41,7 +41,7 @@
<q-btn
v-else
flat
color="deep-purple"
color="primary"
type="a"
:href="['{{ url_for('core.extensions') }}', '?usr=', g.user.id, '&enable=', extension.code].join('')"
>

View File

@ -8,7 +8,7 @@
{% if lnurl %}
<q-btn
unelevated
color="deep-purple"
color="primary"
@click="processing"
type="a"
href="{{ url_for('core.lnurlwallet', lightning=lnurl) }}"
@ -25,7 +25,7 @@
></q-input>
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="walletName == ''"
type="submit"
>Add a new wallet</q-btn
@ -88,7 +88,12 @@
<div class="col-12 col-md-3 col-lg-3">
<div class="row q-col-gutter-lg justify-center">
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
<q-btn flat color="purple" label="Runs on" class="full-width"></q-btn>
<q-btn
flat
color="secondary"
label="Runs on"
class="full-width"
></q-btn>
<div class="row">
<div class="col">
<a href="https://github.com/ElementsProject/lightning">

View File

@ -22,7 +22,7 @@
<div class="col">
<q-btn
unelevated
color="deep-purple"
color="primary"
class="full-width"
@click="showParseDialog"
>Paste Request</q-btn
@ -31,7 +31,7 @@
<div class="col">
<q-btn
unelevated
color="deep-purple"
color="primary"
class="full-width"
@click="showReceiveDialog"
>Create Invoice</q-btn
@ -40,7 +40,7 @@
<div class="col">
<q-btn
unelevated
color="purple"
color="secondary"
icon="photo_camera"
@click="showCamera"
>scan
@ -313,12 +313,21 @@
<b>{{receive.lnurl.domain}}</b> is requesting an invoice:
</p>
<q-select
filled
dense
v-model="receive.unit"
type="text"
label="Unit"
:options="receive.units"
></q-select>
<q-input
filled
dense
v-model.number="receive.data.amount"
type="number"
label="Amount (sat) *"
:label="`Amount (${receive.unit}) *`"
:step="receive.unit != 'sat' ? '0.001' : '1'"
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
@ -333,7 +342,7 @@
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="receive.data.memo == null || receive.data.amount == null || receive.data.amount <= 0"
type="submit"
>
@ -346,7 +355,7 @@
</div>
<q-spinner
v-if="receive.status == 'loading'"
color="deep-purple"
color="primary"
size="2.55em"
></q-spinner>
</q-form>
@ -386,7 +395,7 @@
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn unelevated color="deep-purple" @click="payInvoice">Pay</q-btn>
<q-btn unelevated color="primary" @click="payInvoice">Pay</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
<div v-else class="row q-mt-lg">
@ -414,7 +423,7 @@
<code class="text-wrap"> {{ parse.lnurlauth.pubkey }} </code>
</p>
<div class="row q-mt-lg">
<q-btn unelevated color="deep-purple" type="submit">Login</q-btn>
<q-btn unelevated color="primary" type="submit">Login</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
@ -476,9 +485,7 @@
</div>
</div>
<div class="row q-mt-lg">
<q-btn unelevated color="deep-purple" type="submit"
>Send satoshis</q-btn
>
<q-btn unelevated color="primary" type="submit">Send satoshis</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
@ -503,7 +510,7 @@
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="parse.data.request == ''"
type="submit"
>Read</q-btn

View File

@ -1,4 +1,4 @@
import trio # type: ignore
import trio
import json
import lnurl # type: ignore
import httpx
@ -10,6 +10,7 @@ from typing import Dict, Union
from lnbits import bolt11
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis
from .. import core_app, db
from ..crud import save_balance_check
@ -47,13 +48,14 @@ async def api_payments():
@api_check_wallet_key("invoice")
@api_validate_post_request(
schema={
"amount": {"type": "integer", "min": 1, "required": True},
"amount": {"type": "number", "min": 0.001, "required": True},
"memo": {
"type": "string",
"empty": False,
"required": True,
"excludes": "description_hash",
},
"unit": {"type": "string", "empty": False, "required": False},
"description_hash": {
"type": "string",
"empty": False,
@ -74,11 +76,17 @@ async def api_payments_create_invoice():
description_hash = b""
memo = g.data["memo"]
if g.data.get("unit") or "sat" == "sat":
amount = g.data["amount"]
else:
price_in_sats = await fiat_amount_as_satoshis(g.data["amount"], g.data["unit"])
amount = price_in_sats
async with db.connect() as conn:
try:
payment_hash, payment_request = await create_invoice(
wallet_id=g.wallet.id,
amount=g.data["amount"],
amount=amount,
memo=memo,
description_hash=description_hash,
extra=g.data.get("extra"),
@ -435,3 +443,8 @@ async def api_perform_lnurlauth():
if err:
return jsonify({"reason": err.reason}), HTTPStatus.SERVICE_UNAVAILABLE
return "", HTTPStatus.OK
@core_app.route("/api/v1/currencies", methods=["GET"])
async def api_list_currencies_available():
return jsonify(list(currencies.keys()))

View File

@ -1,4 +1,4 @@
import trio # type: ignore
import trio
import datetime
from http import HTTPStatus
from quart import jsonify
@ -32,6 +32,24 @@ async def api_public_payment_longpolling(payment_hash):
print("adding standalone invoice listener", payment_hash, send_payment)
api_invoice_listeners.append(send_payment)
async for payment in receive_payment:
if payment.payment_hash == payment_hash:
return jsonify({"status": "paid"}), HTTPStatus.OK
response = None
async def payment_info_receiver(cancel_scope):
async for payment in receive_payment:
if payment.payment_hash == payment_hash:
nonlocal response
response = (jsonify({"status": "paid"}), HTTPStatus.OK)
cancel_scope.cancel()
async def timeouter(cancel_scope):
await trio.sleep(45)
cancel_scope.cancel()
async with trio.open_nursery() as nursery:
nursery.start_soon(payment_info_receiver, nursery.cancel_scope)
nursery.start_soon(timeouter, nursery.cancel_scope)
if response:
return response
else:
return jsonify({"message": "timeout"}), HTTPStatus.REQUEST_TIMEOUT

View File

@ -1,2 +0,0 @@
*
!.gitignore

View File

@ -3,7 +3,7 @@ import trio
from contextlib import asynccontextmanager
from sqlalchemy import create_engine # type: ignore
from sqlalchemy_aio import TRIO_STRATEGY # type: ignore
from sqlalchemy_aio.base import AsyncConnection
from sqlalchemy_aio.base import AsyncConnection # type: ignore
from .settings import LNBITS_DATA_FOLDER

View File

@ -4,7 +4,7 @@
<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"
<q-btn unelevated color="primary" @click="amilkDialog.show = true"
>New AMilk</q-btn
>
</q-card-section>
@ -109,7 +109,7 @@
></q-input>
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="amilkDialog.data.amount == null || amilkDialog.data.amount < 0 || amilkDialog.data.lnurl == null"
type="submit"
>Create amilk</q-btn

View File

@ -11,7 +11,7 @@
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
<q-btn unelevated color="primary" @click="formDialog.show = true"
>Add Bleskomat</q-btn
>
</q-card-section>
@ -150,14 +150,14 @@
<q-btn
v-if="formDialog.data.id"
unelevated
color="deep-purple"
color="primary"
type="submit"
>Update Bleskomat</q-btn
>
<q-btn
v-else
unelevated
color="deep-purple"
color="primary"
:disable="
formDialog.data.wallet == null ||
formDialog.data.name == null ||

View File

@ -24,7 +24,7 @@
dense
flat
icon="check"
color="deep-purple"
color="primary"
type="submit"
@click="createInvoice"
:disabled="userAmount < captchaAmount || paymentReq"

View File

@ -4,7 +4,7 @@
<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"
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New captcha</q-btn
>
</q-card-section>
@ -141,7 +141,7 @@
<q-item-section avatar>
<q-checkbox
v-model="formDialog.data.remembers"
color="deep-purple"
color="primary"
></q-checkbox>
</q-item-section>
<q-item-section>
@ -157,7 +157,7 @@
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="formDialog.data.amount == null || formDialog.data.amount < 0 || formDialog.data.memo == null"
type="submit"
>Create captcha</q-btn

View File

@ -0,0 +1,3 @@
# StreamerCopilot
Tool to help streamers accept sats for tips

View File

@ -0,0 +1,17 @@
from quart import Blueprint
from lnbits.db import Database
db = Database("ext_copilot")
copilot_ext: Blueprint = Blueprint(
"copilot", __name__, static_folder="static", template_folder="templates"
)
from .views_api import * # noqa
from .views import * # noqa
from .lnurl import * # noqa
from .tasks import register_listeners
from lnbits.tasks import record_async
copilot_ext.record(record_async(register_listeners))

View File

@ -0,0 +1,8 @@
{
"name": "StreamerCopilot",
"short_description": "Video tips/animations/webhooks",
"icon": "face",
"contributors": [
"arcbtc"
]
}

View File

@ -0,0 +1,109 @@
from typing import List, Optional, Union
# from lnbits.db import open_ext_db
from . import db
from .models import Copilots
from lnbits.helpers import urlsafe_short_hash
from quart import jsonify
###############COPILOTS##########################
async def create_copilot(
title: str,
user: str,
lnurl_toggle: Optional[int] = 0,
wallet: Optional[str] = None,
animation1: Optional[str] = None,
animation2: Optional[str] = None,
animation3: Optional[str] = None,
animation1threshold: Optional[int] = None,
animation2threshold: Optional[int] = None,
animation3threshold: Optional[int] = None,
animation1webhook: Optional[str] = None,
animation2webhook: Optional[str] = None,
animation3webhook: Optional[str] = None,
lnurl_title: Optional[str] = None,
show_message: Optional[int] = 0,
show_ack: Optional[int] = 0,
show_price: Optional[int] = 0,
amount_made: Optional[int] = None,
) -> Copilots:
copilot_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO copilots (
id,
user,
lnurl_toggle,
wallet,
title,
animation1,
animation2,
animation3,
animation1threshold,
animation2threshold,
animation3threshold,
animation1webhook,
animation2webhook,
animation3webhook,
lnurl_title,
show_message,
show_ack,
show_price,
lnurl_title,
amount_made
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
copilot_id,
user,
lnurl_toggle,
wallet,
title,
animation1,
animation2,
animation3,
animation1threshold,
animation2threshold,
animation3threshold,
animation1webhook,
animation2webhook,
animation3webhook,
lnurl_title,
show_message,
show_ack,
show_price,
lnurl_title,
0,
),
)
return await get_copilot(copilot_id)
async def update_copilot(copilot_id: str, **kwargs) -> Optional[Copilots]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE copilots SET {q} WHERE id = ?", (*kwargs.values(), copilot_id)
)
row = await db.fetchone("SELECT * FROM copilots WHERE id = ?", (copilot_id,))
return Copilots.from_row(row) if row else None
async def get_copilot(copilot_id: str) -> Copilots:
row = await db.fetchone("SELECT * FROM copilots WHERE id = ?", (copilot_id,))
return Copilots.from_row(row) if row else None
async def get_copilots(user: str) -> List[Copilots]:
rows = await db.fetchall("SELECT * FROM copilots WHERE user = ?", (user,))
return [Copilots.from_row(row) for row in rows]
async def delete_copilot(copilot_id: str) -> None:
await db.execute("DELETE FROM copilots WHERE id = ?", (copilot_id,))

View File

@ -0,0 +1,86 @@
import json
import hashlib
import math
from quart import jsonify, url_for, request
from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore
from lnurl.types import LnurlPayMetadata
from lnbits.core.services import create_invoice
from . import copilot_ext
from .crud import get_copilot
@copilot_ext.route("/lnurl/<cp_id>", methods=["GET"])
async def lnurl_response(cp_id):
cp = await get_copilot(cp_id)
if not cp:
return jsonify({"status": "ERROR", "reason": "Copilot not found."})
resp = LnurlPayResponse(
callback=url_for("copilot.lnurl_callback", cp_id=cp_id, _external=True),
min_sendable=10000,
max_sendable=50000000,
metadata=LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]])),
)
params = resp.dict()
if cp.show_message:
params["commentAllowed"] = 300
return jsonify(params)
@copilot_ext.route("/lnurl/cb/<cp_id>", methods=["GET"])
async def lnurl_callback(cp_id):
cp = await get_copilot(cp_id)
if not cp:
return jsonify({"status": "ERROR", "reason": "Copilot not found."})
amount_received = int(request.args.get("amount"))
if amount_received < 10000:
return (
jsonify(
LnurlErrorResponse(
reason=f"Amount {round(amount_received / 1000)} is smaller than minimum 10 sats."
).dict()
),
)
elif amount_received / 1000 > 10000000:
return (
jsonify(
LnurlErrorResponse(
reason=f"Amount {round(amount_received / 1000)} is greater than maximum 50000."
).dict()
),
)
comment = ""
if request.args.get("comment"):
comment = request.args.get("comment")
if len(comment or "") > 300:
return jsonify(
LnurlErrorResponse(
reason=f"Got a comment with {len(comment)} characters, but can only accept 300"
).dict()
)
if len(comment) < 1:
comment = "none"
payment_hash, payment_request = await create_invoice(
wallet_id=cp.wallet,
amount=int(amount_received / 1000),
memo=cp.lnurl_title,
description_hash=hashlib.sha256(
(
LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]]))
).encode("utf-8")
).digest(),
extra={"tag": "copilot", "copilot": cp.id, "comment": comment},
)
resp = LnurlPayActionResponse(
pr=payment_request,
success_action=None,
disposable=False,
routes=[],
)
return jsonify(resp.dict())

View File

@ -0,0 +1,33 @@
async def m001_initial(db):
"""
Initial copilot table.
"""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS copilots (
id TEXT NOT NULL PRIMARY KEY,
user TEXT,
title TEXT,
lnurl_toggle INTEGER,
wallet TEXT,
animation1 TEXT,
animation2 TEXT,
animation3 TEXT,
animation1threshold INTEGER,
animation2threshold INTEGER,
animation3threshold INTEGER,
animation1webhook TEXT,
animation2webhook TEXT,
animation3webhook TEXT,
lnurl_title TEXT,
show_message INTEGER,
show_ack INTEGER,
show_price INTEGER,
amount_made INTEGER,
fullscreen_cam INTEGER,
iframe_url TEXT,
timestamp TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
);
"""
)

View File

@ -0,0 +1,41 @@
from sqlite3 import Row
from typing import NamedTuple
import time
from quart import url_for
from lnurl import Lnurl, encode as lnurl_encode # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
class Copilots(NamedTuple):
id: str
user: str
title: str
lnurl_toggle: int
wallet: str
animation1: str
animation2: str
animation3: str
animation1threshold: int
animation2threshold: int
animation3threshold: int
animation1webhook: str
animation2webhook: str
animation3webhook: str
lnurl_title: str
show_message: int
show_ack: int
show_price: int
amount_made: int
timestamp: int
fullscreen_cam: int
iframe_url: str
@classmethod
def from_row(cls, row: Row) -> "Copilots":
return cls(**dict(row))
@property
def lnurl(self) -> Lnurl:
url = url_for("copilot.lnurl_response", cp_id=self.id, _external=True)
return lnurl_encode(url)

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

View File

@ -0,0 +1,88 @@
import trio # type: ignore
import json
import httpx
from quart import g, jsonify, url_for, websocket
from http import HTTPStatus
from lnbits.core import db as core_db
from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener
from .crud import get_copilot
from .views import updater
import shortuuid
async def register_listeners():
invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2)
register_invoice_listener(invoice_paid_chan_send)
await wait_for_paid_invoices(invoice_paid_chan_recv)
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
async for payment in invoice_paid_chan:
await on_invoice_paid(payment)
async def on_invoice_paid(payment: Payment) -> None:
webhook = None
data = None
if "copilot" != payment.extra.get("tag"):
# not an copilot invoice
return
if payment.extra.get("wh_status"):
# this webhook has already been sent
return
copilot = await get_copilot(payment.extra.get("copilot", -1))
if not copilot:
return (
jsonify({"message": "Copilot link link does not exist."}),
HTTPStatus.NOT_FOUND,
)
if copilot.animation1threshold:
if int(payment.amount / 1000) >= copilot.animation1threshold:
data = copilot.animation1
webhook = copilot.animation1webhook
if copilot.animation2threshold:
if int(payment.amount / 1000) >= copilot.animation2threshold:
data = copilot.animation2
webhook = copilot.animation1webhook
if copilot.animation3threshold:
if int(payment.amount / 1000) >= copilot.animation3threshold:
data = copilot.animation3
webhook = copilot.animation1webhook
if webhook:
async with httpx.AsyncClient() as client:
try:
r = await client.post(
webhook,
json={
"payment_hash": payment.payment_hash,
"payment_request": payment.bolt11,
"amount": payment.amount,
"comment": payment.extra.get("comment"),
},
timeout=40,
)
await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError):
await mark_webhook_sent(payment, -1)
if payment.extra.get("comment"):
await updater(copilot.id, data, payment.extra.get("comment"))
else:
await updater(copilot.id, data, "none")
async def mark_webhook_sent(payment: Payment, status: int) -> None:
payment.extra["wh_status"] = status
await core_db.execute(
"""
UPDATE apipayments SET extra = ?
WHERE hash = ?
""",
(json.dumps(payment.extra), payment.payment_hash),
)

View File

@ -0,0 +1,172 @@
<q-card>
<q-card-section>
<p>
StreamerCopilot: get tips via static QR (lnurl-pay) and show an
animation<br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
<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="Create copilot">
<q-card>
<q-card-section>
<code
><span class="text-blue">POST</span> /copilot/api/v1/copilot</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>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}api/v1/copilot -d '{"title":
&lt;string&gt;, "animation": &lt;string&gt;,
"show_message":&lt;string&gt;, "amount": &lt;integer&gt;,
"lnurl_title": &lt;string&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="Update copilot">
<q-card>
<q-card-section>
<code
><span class="text-blue">PUT</span>
/copilot/api/v1/copilot/&lt;copilot_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">
Body (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root
}}api/v1/copilot/&lt;copilot_id&gt; -d '{"title": &lt;string&gt;,
"animation": &lt;string&gt;, "show_message":&lt;string&gt;,
"amount": &lt;integer&gt;, "lnurl_title": &lt;string&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="Get copilot">
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/copilot/api/v1/copilot/&lt;copilot_id&gt;</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;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}api/v1/copilot/&lt;copilot_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 copilots">
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span> /copilot/api/v1/copilots</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;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}api/v1/copilots -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 a pay link"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/copilot/api/v1/copilot/&lt;copilot_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
}}api/v1/copilot/&lt;copilot_id&gt; -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="Trigger an animation"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/api/v1/copilot/ws/&lt;copilot_id&gt;/&lt;comment&gt;/&lt;data&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 200</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}/api/v1/copilot/ws/&lt;string,
copilot_id&gt;/&lt;string, comment&gt;/&lt;string, gif name&gt; -H
"X-Api-Key: {{ g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
</q-card>

View File

@ -0,0 +1,289 @@
{% extends "public.html" %} {% block page %}<q-page>
<video
autoplay="true"
id="videoScreen"
style="width: 100%"
class="fixed-bottom-right"
></video>
<video
autoplay="true"
id="videoCamera"
style="width: 100%"
class="fixed-bottom-right"
></video>
<img src="" style="width: 100%" id="animations" class="fixed-bottom-left" />
<div
v-if="copilot.lnurl_toggle == 1"
class="rounded-borders column fixed-right"
style="
width: 250px;
background-color: white;
height: 300px;
margin-top: 10%;
"
>
<div class="col">
<qrcode
:value="copilot.lnurl"
:options="{width:250}"
class="rounded-borders"
></qrcode>
<center class="absolute-bottom" style="color: black; font-size: 20px">
{% raw %}{{ copilot.lnurl_title }}{% endraw %}
</center>
</div>
</div>
<h2
v-if="copilot.show_price != 0"
class="text-bold fixed-bottom-left"
style="
margin: 60px 60px;
font-size: 110px;
text-shadow: 4px 8px 4px black;
color: white;
"
>
{% raw %}{{ price }}{% endraw %}
</h2>
<p
v-if="copilot.show_ack != 0"
class="fixed-top"
style="
font-size: 22px;
text-shadow: 2px 4px 1px black;
color: white;
padding-left: 40%;
"
>
Powered by LNbits/StreamerCopilot
</p>
</q-page>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<style>
body.body--dark .q-drawer,
body.body--dark .q-footer,
body.body--dark .q-header,
.q-drawer,
.q-footer,
.q-header {
display: none;
}
.q-page {
padding: 0px;
}
</style>
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data() {
return {
price: '',
counter: 1,
colours: ['teal', 'purple', 'indigo', 'pink', 'green'],
copilot: {},
animQueue: [],
queue: false,
lnurl: ''
}
},
methods: {
showNotif: function (userMessage) {
var colour = this.colours[
Math.floor(Math.random() * this.colours.length)
]
this.$q.notify({
color: colour,
icon: 'chat_bubble_outline',
html: true,
message: '<h4 style="color: white;">' + userMessage + '</h4>',
position: 'top-left',
timeout: 5000
})
},
openURL: function (url) {
return Quasar.utils.openURL(url)
},
initCamera() {
var video = document.querySelector('#videoCamera')
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({video: true})
.then(function (stream) {
video.srcObject = stream
})
.catch(function (err0r) {
console.log('Something went wrong!')
})
}
},
initScreenShare() {
var video = document.querySelector('#videoScreen')
navigator.mediaDevices
.getDisplayMedia({video: true})
.then(function (stream) {
video.srcObject = stream
})
.catch(function (err0r) {
console.log('Something went wrong!')
})
},
pushAnim(content) {
document.getElementById('animations').style.width = content[0]
document.getElementById('animations').src = content[1]
if (content[2] != 'none') {
self.showNotif(content[2])
}
setTimeout(function () {
document.getElementById('animations').src = ''
}, 5000)
},
launch() {
self = this
LNbits.api
.request(
'GET',
'/copilot/api/v1/copilot/ws/' +
self.copilot.id +
'/launching/rocket'
)
.then(function (response1) {
self.$q.notify({
color: 'green',
message: 'Sent!'
})
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
}
},
mounted() {
this.initCamera()
},
created: function () {
self = this
self.copilot = JSON.parse(localStorage.getItem('copilot'))
LNbits.api
.request(
'GET',
'/copilot/api/v1/copilot/' + self.copilot.id,
localStorage.getItem('inkey')
)
.then(function (response) {
self.copilot = response.data
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
this.connectionBitStamp = new WebSocket('wss://ws.bitstamp.net')
const obj = JSON.stringify({
event: 'bts:subscribe',
data: {channel: 'live_trades_' + self.copilot.show_price}
})
this.connectionBitStamp.onmessage = function (e) {
if (self.copilot.show_price) {
if (self.copilot.show_price == 'btcusd') {
self.price = String(
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(JSON.parse(e.data).data.price)
)
} else if (self.copilot.show_price == 'btceur') {
self.price = String(
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(JSON.parse(e.data).data.price)
)
} else if (self.copilot.show_price == 'btcgbp') {
self.price = String(
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'GBP'
}).format(JSON.parse(e.data).data.price)
)
}
}
}
this.connectionBitStamp.onopen = () => this.connectionBitStamp.send(obj)
const fetch = data =>
new Promise(resolve => setTimeout(resolve, 5000, this.pushAnim(data)))
const addTask = (() => {
let pending = Promise.resolve()
const run = async data => {
try {
await pending
} finally {
return fetch(data)
}
}
return data => (pending = run(data))
})()
if (location.protocol !== 'http:') {
localUrl =
'wss://' +
document.domain +
':' +
location.port +
'/copilot/ws/' +
self.copilot.id +
'/'
} else {
localUrl =
'ws://' +
document.domain +
':' +
location.port +
'/copilot/ws/' +
self.copilot.id +
'/'
}
this.connection = new WebSocket(localUrl)
this.connection.onmessage = function (e) {
res = e.data.split('-')
if (res[0] == 'rocket') {
addTask(['40%', '/copilot/static/rocket.gif', res[1]])
}
if (res[0] == 'face') {
addTask(['35%', '/copilot/static/face.gif', res[1]])
}
if (res[0] == 'bitcoin') {
addTask(['30%', '/copilot/static/bitcoin.gif', res[1]])
}
if (res[0] == 'confetti') {
addTask(['100%', '/copilot/static/confetti.gif', res[1]])
}
if (res[0] == 'martijn') {
addTask(['40%', '/copilot/static/martijn.gif', res[1]])
}
if (res[0] == 'rick') {
addTask(['40%', '/copilot/static/rick.gif', res[1]])
}
if (res[0] == 'true') {
document.getElementById('videoCamera').style.width = '20%'
self.initScreenShare()
}
if (res[0] == 'false') {
document.getElementById('videoCamera').style.width = '100%'
document.getElementById('videoScreen').src = null
}
}
this.connection.onopen = () => this.launch
}
})
</script>
{% endblock %}

View File

@ -0,0 +1,637 @@
{% 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-7 q-gutter-y-md">
<q-card>
<q-card-section>
{% raw %}
<q-btn unelevated color="primary" @click="formDialogCopilot.show = true"
>New copilot instance
</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">Copilots</h5>
</div>
<div class="col-auto">
<q-input
borderless
dense
debounce="300"
v-model="filter"
placeholder="Search"
>
<template v-slot:append>
<q-icon name="search"></q-icon>
</template>
</q-input>
<q-btn flat color="grey" @click="exportcopilotCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
flat
dense
:data="CopilotLinks"
row-key="id"
:columns="CopilotsTable.columns"
:pagination.sync="CopilotsTable.pagination"
:filter="filter"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th style="width: 5%"></q-th>
<q-th style="width: 5%"></q-th>
<q-th style="width: 5%"></q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.label }}</div>
</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="apps"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openCopilotPanel(props.row.id)"
>
<q-tooltip> Panel </q-tooltip>
</q-btn>
</q-td>
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="face"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openCopilotCompose(props.row.id)"
>
<q-tooltip> Compose window </q-tooltip>
</q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteCopilotLink(props.row.id)"
icon="cancel"
color="pink"
>
<q-tooltip> Delete copilot </q-tooltip>
</q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="openUpdateCopilotLink(props.row.id)"
icon="edit"
color="light-blue"
>
<q-tooltip> Edit copilot </q-tooltip>
</q-btn>
</q-td>
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.value }}</div>
</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 StreamCopilot Extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "copilot/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog
v-model="formDialogCopilot.show"
position="top"
@hide="closeFormDialog"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormDataCopilot" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialogCopilot.data.title"
type="text"
label="Title"
></q-input>
<div class="row">
<q-checkbox
v-model="formDialogCopilot.data.lnurl_toggle"
label="Include lnurl payment QR? (requires https)"
left-label
></q-checkbox>
</div>
<div v-if="formDialogCopilot.data.lnurl_toggle">
<q-checkbox
v-model="formDialogCopilot.data.show_message"
left-label
label="Show lnurl-pay messages? (supported by few wallets)"
></q-checkbox>
<q-select
filled
dense
emit-value
v-model="formDialogCopilot.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
<q-expansion-item
group="api"
dense
expand-separator
label="Payment threshold 1"
>
<q-card>
<q-card-section>
<div class="row">
<div class="col">
<q-select
filled
dense
v-model.trim="formDialogCopilot.data.animation1"
:options="options"
label="Animation"
/>
</div>
<div class="col q-pl-xs">
<q-input
filled
dense
v-model.trim="formDialogCopilot.data.animation1threshold"
type="number"
label="From *sats"
:min="10"
>
</q-input>
</div>
<div class="col q-pl-xs">
<q-input
filled
dense
v-model.trim="formDialogCopilot.data.animation1webhook"
type="text"
label="Webhook"
>
</q-input>
</div>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Payment threshold 2 (Must be higher than last)"
>
<q-card>
<q-card-section>
<div
class="row"
v-if="formDialogCopilot.data.animation1threshold > 0"
>
<div class="col">
<q-select
filled
dense
v-model.trim="formDialogCopilot.data.animation2"
:options="options"
label="Animation"
/>
</div>
<div class="col q-pl-xs">
<q-input
filled
dense
v-model="formDialogCopilot.data.animation2threshold"
type="number"
label="From *sats"
:min="formDialogCopilot.data.animation1threshold"
>
</q-input>
</div>
<div class="col q-pl-xs">
<q-input
filled
dense
v-model.trim="formDialogCopilot.data.animation2webhook"
type="text"
label="Webhook"
>
</q-input>
</div>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Payment threshold 3 (Must be higher than last)"
>
<q-card>
<q-card-section>
<div
class="row"
v-if="formDialogCopilot.data.animation2threshold > formDialogCopilot.data.animation1threshold"
>
<div class="col">
<q-select
filled
dense
v-model.trim="formDialogCopilot.data.animation3"
:options="options"
label="Animation"
/>
</div>
<div class="col q-pl-xs">
<q-input
filled
dense
v-model="formDialogCopilot.data.animation3threshold"
type="number"
label="From *sats"
:min="formDialogCopilot.data.animation2threshold"
>
</q-input>
</div>
<div class="col q-pl-xs">
<q-input
filled
dense
v-model.trim="formDialogCopilot.data.animation3webhook"
type="text"
label="Webhook"
>
</q-input>
</div>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<q-input
filled
dense
v-model.trim="formDialogCopilot.data.lnurl_title"
type="text"
max="1440"
label="Lnurl title (message with QR code)"
>
</q-input>
</div>
<div class="q-gutter-sm">
<q-select
filled
dense
style="width: 50%"
v-model.trim="formDialogCopilot.data.show_price"
:options="currencyOptions"
label="Show price"
/>
</div>
<div class="q-gutter-sm">
<div class="row">
<q-checkbox
v-model="formDialogCopilot.data.show_ack"
left-label
label="Show 'powered by LNbits'"
></q-checkbox>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
v-if="formDialogCopilot.data.id"
unelevated
color="primary"
:disable="
formDialogCopilot.data.title == ''"
type="submit"
>Update Copilot</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="
formDialogCopilot.data.title == ''"
type="submit"
>Create Copilot</q-btn
>
<q-btn @click="cancelCopilot" 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 src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<style></style>
<script>
Vue.component(VueQrcode.name, VueQrcode)
var mapCopilot = obj => {
obj._data = _.clone(obj)
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
obj.time = obj.time + 'mins'
if (obj.time_elapsed) {
obj.date = 'Time elapsed'
} else {
obj.date = Quasar.utils.date.formatDate(
new Date((obj.theTime - 3600) * 1000),
'HH:mm:ss'
)
}
obj.displayComposeUrl = ['/copilot/cp/', obj.id].join('')
obj.displayPanelUrl = ['/copilot/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
filter: '',
CopilotLinks: [],
CopilotLinksObj: [],
CopilotsTable: {
columns: [
{
name: 'theId',
align: 'left',
label: 'id',
field: 'id'
},
{
name: 'lnurl_toggle',
align: 'left',
label: 'Show lnurl pay link',
field: 'lnurl_toggle'
},
{
name: 'title',
align: 'left',
label: 'title',
field: 'title'
},
{
name: 'amount_made',
align: 'left',
label: 'amount made',
field: 'amount_made'
}
],
pagination: {
rowsPerPage: 10
}
},
passedCopilot: {},
formDialog: {
show: false,
data: {}
},
formDialogCopilot: {
show: false,
data: {
lnurl_toggle: false,
show_message: false,
show_ack: false,
show_price: 'None',
title: ''
}
},
qrCodeDialog: {
show: false,
data: null
},
options: ['bitcoin', 'confetti', 'rocket', 'face', 'martijn', 'rick'],
currencyOptions: ['None', 'btcusd', 'btceur', 'btcgbp']
}
},
methods: {
cancelCopilot: function (data) {
var self = this
self.formDialogCopilot.show = false
},
closeFormDialog: function () {
this.formDialog.data = {
is_unique: false
}
},
sendFormDataCopilot: function () {
var self = this
if (self.formDialogCopilot.data.id) {
this.updateCopilot(
self.g.user.wallets[0].adminkey,
self.formDialogCopilot.data
)
} else {
this.createCopilot(
self.g.user.wallets[0].adminkey,
self.formDialogCopilot.data
)
}
},
createCopilot: function (wallet, data) {
var self = this
var updatedData = {}
for (const property in data) {
if (data[property]) {
updatedData[property] = data[property]
}
if (property == 'animation1threshold' && data[property]) {
updatedData[property] = parseInt(data[property])
}
if (property == 'animation2threshold' && data[property]) {
updatedData[property] = parseInt(data[property])
}
if (property == 'animation3threshold' && data[property]) {
updatedData[property] = parseInt(data[property])
}
}
LNbits.api
.request('POST', '/copilot/api/v1/copilot', wallet, updatedData)
.then(function (response) {
self.CopilotLinks.push(mapCopilot(response.data))
self.formDialogCopilot.show = false
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getCopilots: function () {
var self = this
LNbits.api
.request(
'GET',
'/copilot/api/v1/copilot',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.CopilotLinks = response.data.map(mapCopilot)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getCopilot: function (copilot_id) {
var self = this
LNbits.api
.request(
'GET',
'/copilot/api/v1/copilot/' + copilot_id,
this.g.user.wallets[0].inkey
)
.then(function (response) {
localStorage.setItem('copilot', JSON.stringify(response.data))
localStorage.setItem('inkey', self.g.user.wallets[0].inkey)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
openCopilotCompose: function (copilot_id) {
this.getCopilot(copilot_id)
let params =
'scrollbars=no, resizable=no,status=no,location=no,toolbar=no,menubar=no,width=1200,height=644,left=410,top=100'
open('../copilot/cp/', '_blank', params)
},
openCopilotPanel: function (copilot_id) {
this.getCopilot(copilot_id)
let params =
'scrollbars=no, resizable=no,status=no,location=no,toolbar=no,menubar=no,width=300,height=450,left=10,top=400'
open('../copilot/pn/', '_blank', params)
},
deleteCopilotLink: function (copilotId) {
var self = this
var link = _.findWhere(this.CopilotLinks, {id: copilotId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this pay link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/copilot/api/v1/copilot/' + copilotId,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
self.CopilotLinks = _.reject(self.CopilotLinks, function (obj) {
return obj.id === copilotId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
openUpdateCopilotLink: function (copilotId) {
var self = this
var copilot = _.findWhere(this.CopilotLinks, {id: copilotId})
self.formDialogCopilot.data = _.clone(copilot._data)
self.formDialogCopilot.show = true
},
updateCopilot: function (wallet, data) {
var self = this
var updatedData = {}
for (const property in data) {
if (data[property]) {
updatedData[property] = data[property]
}
if (property == 'animation1threshold' && data[property]) {
updatedData[property] = parseInt(data[property])
}
if (property == 'animation2threshold' && data[property]) {
updatedData[property] = parseInt(data[property])
}
if (property == 'animation3threshold' && data[property]) {
updatedData[property] = parseInt(data[property])
}
}
LNbits.api
.request(
'PUT',
'/copilot/api/v1/copilot/' + updatedData.id,
wallet,
updatedData
)
.then(function (response) {
self.CopilotLinks = _.reject(self.CopilotLinks, function (obj) {
return obj.id === updatedData.id
})
self.CopilotLinks.push(mapCopilot(response.data))
self.formDialogCopilot.show = false
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
exportcopilotCSV: function () {
var self = this
LNbits.utils.exportCSV(self.CopilotsTable.columns, this.CopilotLinks)
}
},
created: function () {
var self = this
var getCopilots = this.getCopilots
getCopilots()
}
})
</script>
{% endblock %}

View File

@ -0,0 +1,157 @@
{% extends "public.html" %} {% block page %}
<div class="q-pa-sm" style="width: 240px; margin: 10px auto">
<q-card class="my-card">
<div class="column">
<div class="col">
<center>
<q-btn
flat
round
dense
@click="openCompose"
icon="face"
style="font-size: 60px"
></q-btn>
</center>
</div>
<center>
<div class="col" style="margin: 15px; font-size: 22px">
Title: {% raw %} {{ copilot.title }} {% endraw %}
</div>
</center>
<q-separator></q-separator>
<div class="col">
<div class="row">
<div class="col">
<q-btn
class="q-mt-sm q-ml-sm"
color="primary"
@click="fullscreenToggle"
label="Screen share"
size="sm"
>
</q-btn>
</div>
</div>
<div class="row q-pa-sm">
<div class="col">
<q-btn
style="width: 95%"
color="primary"
@click="animationBTN('rocket')"
label="rocket"
size="sm"
/>
</div>
<div class="col">
<q-btn
style="width: 95%"
color="primary"
@click="animationBTN('confetti')"
label="confetti"
size="sm"
/>
</div>
<div class="col">
<q-btn
style="width: 95%"
color="primary"
@click="animationBTN('face')"
label="face"
size="sm"
/>
</div>
</div>
<div class="row q-pa-sm">
<div class="col">
<q-btn
style="width: 95%"
color="primary"
@click="animationBTN('rick')"
label="rick"
size="sm"
/>
</div>
<div class="col">
<q-btn
style="width: 95%"
color="primary"
@click="animationBTN('martijn')"
label="martijn"
size="sm"
/>
</div>
<div class="col">
<q-btn
style="width: 95%"
color="primary"
@click="animationBTN('bitcoin')"
label="bitcoin"
size="sm"
/>
</div>
</div>
</div>
</div>
</q-card>
</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)
new Vue({
el: '#vue',
mixins: [windowMixin],
data() {
return {
fullscreen_cam: true,
textareaModel: '',
iframe: '',
copilot: {}
}
},
methods: {
iframeChange: function (url) {
this.connection.send(String(url))
},
fullscreenToggle: function () {
self = this
self.animationBTN(String(this.fullscreen_cam))
if (this.fullscreen_cam) {
this.fullscreen_cam = false
} else {
this.fullscreen_cam = true
}
},
openCompose: function () {
let params =
'scrollbars=no, resizable=no,status=no,location=no,toolbar=no,menubar=no,width=1200,height=644,left=410,top=100'
open('../cp/', 'test', params)
},
animationBTN: function (name) {
self = this
LNbits.api
.request(
'GET',
'/copilot/api/v1/copilot/ws/' + self.copilot.id + '/none/' + name
)
.then(function (response1) {
self.$q.notify({
color: 'green',
message: 'Sent!'
})
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
}
},
created: function () {
self = this
self.copilot = JSON.parse(localStorage.getItem('copilot'))
}
})
</script>
{% endblock %}

View File

@ -0,0 +1,61 @@
from quart import g, abort, render_template, jsonify, websocket
from http import HTTPStatus
import httpx
from collections import defaultdict
from lnbits.decorators import check_user_exists, validate_uuids
from . import copilot_ext
from .crud import get_copilot
from quart import g, abort, render_template, jsonify, websocket
from functools import wraps
import trio
import shortuuid
from . import copilot_ext
@copilot_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def index():
return await render_template("copilot/index.html", user=g.user)
@copilot_ext.route("/cp/")
async def compose():
return await render_template("copilot/compose.html")
@copilot_ext.route("/pn/")
async def panel():
return await render_template("copilot/panel.html")
##################WEBSOCKET ROUTES########################
# socket_relay is a list where the control panel or
# lnurl endpoints can leave a message for the compose window
connected_websockets = defaultdict(set)
@copilot_ext.websocket("/ws/<id>/")
async def wss(id):
copilot = await get_copilot(id)
if not copilot:
return "", HTTPStatus.FORBIDDEN
global connected_websockets
send_channel, receive_channel = trio.open_memory_channel(0)
connected_websockets[id].add(send_channel)
try:
while True:
data = await receive_channel.receive()
await websocket.send(data)
finally:
connected_websockets[id].remove(send_channel)
async def updater(copilot_id, data, comment):
copilot = await get_copilot(copilot_id)
if not copilot:
return
for queue in connected_websockets[copilot_id]:
await queue.send(f"{data + '-' + comment}")

View File

@ -0,0 +1,109 @@
import hashlib
from quart import g, jsonify, url_for, websocket
from http import HTTPStatus
import httpx
from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from .views import updater
from . import copilot_ext
from lnbits.extensions.copilot import copilot_ext
from .crud import (
create_copilot,
update_copilot,
get_copilot,
get_copilots,
delete_copilot,
)
#######################COPILOT##########################
@copilot_ext.route("/api/v1/copilot", methods=["POST"])
@copilot_ext.route("/api/v1/copilot/<copilot_id>", methods=["PUT"])
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={
"title": {"type": "string", "empty": False, "required": True},
"lnurl_toggle": {"type": "integer", "empty": False},
"wallet": {"type": "string", "empty": False, "required": False},
"animation1": {"type": "string", "empty": True, "required": False},
"animation2": {"type": "string", "empty": True, "required": False},
"animation3": {"type": "string", "empty": True, "required": False},
"animation1threshold": {"type": "integer", "empty": True, "required": False},
"animation2threshold": {"type": "integer", "empty": True, "required": False},
"animation3threshold": {"type": "integer", "empty": True, "required": False},
"animation1webhook": {"type": "string", "empty": True, "required": False},
"animation2webhook": {"type": "string", "empty": True, "required": False},
"animation3webhook": {"type": "string", "empty": True, "required": False},
"lnurl_title": {"type": "string", "empty": True, "required": False},
"show_message": {"type": "integer", "empty": True, "required": False},
"show_ack": {"type": "integer", "empty": True},
"show_price": {"type": "string", "empty": True},
}
)
async def api_copilot_create_or_update(copilot_id=None):
if not copilot_id:
copilot = await create_copilot(user=g.wallet.user, **g.data)
return jsonify(copilot._asdict()), HTTPStatus.CREATED
else:
copilot = await update_copilot(copilot_id=copilot_id, **g.data)
return jsonify(copilot._asdict()), HTTPStatus.OK
@copilot_ext.route("/api/v1/copilot", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_copilots_retrieve():
try:
return (
jsonify(
[{**copilot._asdict()} for copilot in await get_copilots(g.wallet.user)]
),
HTTPStatus.OK,
)
except:
return ""
@copilot_ext.route("/api/v1/copilot/<copilot_id>", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_copilot_retrieve(copilot_id):
copilot = await get_copilot(copilot_id)
if not copilot:
return jsonify({"message": "copilot does not exist"}), HTTPStatus.NOT_FOUND
if not copilot.lnurl_toggle:
return (
jsonify({**copilot._asdict()}),
HTTPStatus.OK,
)
return (
jsonify({**copilot._asdict(), **{"lnurl": copilot.lnurl}}),
HTTPStatus.OK,
)
@copilot_ext.route("/api/v1/copilot/<copilot_id>", methods=["DELETE"])
@api_check_wallet_key("admin")
async def api_copilot_delete(copilot_id):
copilot = await get_copilot(copilot_id)
if not copilot:
return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND
await delete_copilot(copilot_id)
return "", HTTPStatus.NO_CONTENT
@copilot_ext.route("/api/v1/copilot/ws/<copilot_id>/<comment>/<data>", methods=["GET"])
async def api_copilot_ws_relay(copilot_id, comment, data):
copilot = await get_copilot(copilot_id)
if not copilot:
return jsonify({"message": "copilot does not exist"}), HTTPStatus.NOT_FOUND
try:
await updater(copilot_id, data, comment)
except:
return "", HTTPStatus.FORBIDDEN
return "", HTTPStatus.OK

View File

@ -4,10 +4,10 @@
<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="productDialog.show = true"
<q-btn unelevated color="primary" @click="productDialog.show = true"
>New Product</q-btn
>
<q-btn unelevated color="deep-purple" @click="indexerDialog.show = true"
<q-btn unelevated color="primary" @click="indexerDialog.show = true"
>New Indexer
<q-tooltip>
Frontend shop your stall will list its products in
@ -282,7 +282,7 @@
<q-btn
v-if="productDialog.data.id"
unelevated
color="deep-purple"
color="primary"
type="submit"
>Update Product</q-btn
>
@ -290,7 +290,7 @@
<q-btn
v-else
unelevated
color="deep-purple"
color="primary"
:disable="productDialog.data.image == null
|| productDialog.data.product == null
|| productDialog.data.description == null
@ -374,7 +374,7 @@
<q-btn
v-if="indexerDialog.data.id"
unelevated
color="deep-purple"
color="primary"
type="submit"
>Update Indexer</q-btn
>
@ -382,7 +382,7 @@
<q-btn
v-else
unelevated
color="deep-purple"
color="primary"
:disable="indexerDialog.data.shopname == null
|| indexerDialog.data.shippingzone1 == null
|| indexerDialog.data.indexeraddress == null

View File

@ -26,7 +26,7 @@
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="formDialog.data.name == '' || formDialog.data.email == '' || paymentReq"
type="submit"
>Submit</q-btn
@ -46,7 +46,7 @@
size="xl"
:href="ticketLink.data.link"
target="_blank"
color="deep-purple"
color="primary"
type="a"
>Link to your ticket!</q-btn
>

View File

@ -4,7 +4,7 @@
<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"
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New Event</q-btn
>
</q-card-section>
@ -267,14 +267,14 @@
<q-btn
v-if="formDialog.data.id"
unelevated
color="deep-purple"
color="primary"
type="submit"
>Update Event</q-btn
>
<q-btn
v-else
unelevated
color="deep-purple"
color="primary"
: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

View File

@ -10,7 +10,7 @@
<br />
<q-btn unelevated color="deep-purple" @click="showCamera" size="xl"
<q-btn unelevated color="primary" @click="showCamera" size="xl"
>Scan ticket</q-btn
>
</center>
@ -82,7 +82,7 @@
<script>
Vue.component(VueQrcode.name, VueQrcode)
Vue.use(VueQrcodeReader)
var mapEvents = function(obj) {
var mapEvents = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
@ -94,7 +94,7 @@
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function() {
data: function () {
return {
tickets: [],
ticketsTable: {
@ -119,35 +119,35 @@
}
},
methods: {
hoverEmail: function(tmp) {
hoverEmail: function (tmp) {
this.tickets.data.emailtemp = tmp
},
closeCamera: function() {
closeCamera: function () {
this.sendCamera.show = false
},
showCamera: function() {
showCamera: function () {
this.sendCamera.show = true
},
decodeQR: function(res) {
decodeQR: function (res) {
this.sendCamera.show = false
var self = this
LNbits.api
.request('GET', '/events/api/v1/register/ticket/' + res)
.then(function(response) {
.then(function (response) {
self.$q.notify({
type: 'positive',
message: 'Registered!'
})
setTimeout(function() {
setTimeout(function () {
window.location.reload()
}, 2000)
})
.catch(function(error) {
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getEventTickets: function() {
getEventTickets: function () {
var self = this
console.log('obj')
LNbits.api
@ -155,17 +155,17 @@
'GET',
'/events/api/v1/eventtickets/{{ wallet_id }}/{{ event_id }}'
)
.then(function(response) {
self.tickets = response.data.map(function(obj) {
.then(function (response) {
self.tickets = response.data.map(function (obj) {
return mapEvents(obj)
})
})
.catch(function(error) {
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
},
created: function() {
created: function () {
this.getEventTickets()
}
})

View File

@ -1,5 +1,36 @@
# Jukebox
To use this extension you need a Spotify client ID and client secret. You get these by creating an app in the Spotify developers dashboard here https://developer.spotify.com/dashboard/applications
## An actual Jukebox where users pay sats to play their favourite music from your playlists
Select the playlists you want people to be able to pay for, share the frontend page, profit :)
**Note:** To use this extension you need a Premium Spotify subscription.
## Usage
1. Click on "ADD SPOTIFY JUKEBOX"\
![add jukebox](https://i.imgur.com/NdVoKXd.png)
2. Follow the steps required on the form\
- give your jukebox a name
- select a wallet to receive payment
- define the price a user must pay to select a song\
![pick wallet price](https://i.imgur.com/4bJ8mb9.png)
- follow the steps to get your Spotify App and get the client ID and secret key\
![spotify keys](https://i.imgur.com/w2EzFtB.png)
- paste the codes in the form\
![api keys](https://i.imgur.com/6b9xauo.png)
- copy the _Redirect URL_ presented on the form\
![redirect url](https://i.imgur.com/GMzl0lG.png)
- on Spotify click the "EDIT SETTINGS" button and paste the copied link in the _Redirect URI's_ prompt
![spotify app setting](https://i.imgur.com/vb0x4Tl.png)
- back on LNBits, click "AUTORIZE ACCESS" and "Agree" on the page that will open
- choose on which device the LNBits Jukebox extensions will stream to, you may have to be logged in in order to select the device (browser, smartphone app, etc...)
- and select what playlist will be available for users to choose songs (you need to have already playlist on Spotify)\
![select playlists](https://i.imgur.com/g4dbtED.png)
3. After Jukebox is created, click the icon to open the dialog with the shareable QR, open the Jukebox page, etc...\
![shareable jukebox](https://i.imgur.com/EAh9PI0.png)
4. The users will see the Jukebox page and choose a song from the selected playlist\
![select song](https://i.imgur.com/YYjeQAs.png)
5. After selecting a song they'd like to hear next a dialog will show presenting the music\
![play for sats](https://i.imgur.com/eEHl3o8.png)
6. After payment, the song will automatically start playing on the device selected or enter the queue if some other music is already playing

View File

@ -46,12 +46,6 @@ new Vue({
align: 'left',
label: 'Price',
field: 'price'
},
{
name: 'profit',
align: 'left',
label: 'Profit',
field: 'profit'
}
],
pagination: {
@ -93,7 +87,11 @@ new Vue({
getJukeboxes() {
self = this
LNbits.api
.request('GET', '/jukebox/api/v1/jukebox', self.g.user.wallets[0].adminkey)
.request(
'GET',
'/jukebox/api/v1/jukebox',
self.g.user.wallets[0].adminkey
)
.then(function (response) {
self.JukeboxLinks = response.data.map(mapJukebox)
})
@ -165,10 +163,10 @@ new Vue({
LNbits.utils.notifyApiError(err)
})
},
authAccess() {
authAccess() {
self = this
self.requestAuthorization()
self.getSpotifyTokens()
self.requestAuthorization()
self.getSpotifyTokens()
self.$q.notify({
spinner: true,
message: 'Processing',
@ -195,37 +193,37 @@ new Vue({
if (self.jukeboxDialog.data.sp_access_token) {
self.refreshPlaylists()
self.refreshDevices()
console.log("this.devices")
console.log('this.devices')
console.log(self.devices)
console.log("this.devices")
console.log('this.devices')
setTimeout(function () {
if (self.devices.length < 1 || self.playlists.length < 1) {
self.$q.notify({
spinner: true,
color: 'red',
message:
'Error! Make sure Spotify is open on the device you wish to use, has playlists, and is playing something',
timeout: 10000
})
LNbits.api
.request(
'DELETE',
'/jukebox/api/v1/jukebox/' + response.data.id,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
self.getJukeboxes()
if (self.devices.length < 1 || self.playlists.length < 1) {
self.$q.notify({
spinner: true,
color: 'red',
message:
'Error! Make sure Spotify is open on the device you wish to use, has playlists, and is playing something',
timeout: 10000
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
clearInterval(timerId)
self.closeFormDialog()
} else {
self.step = 4
clearInterval(timerId)
}
}, 2000)
LNbits.api
.request(
'DELETE',
'/jukebox/api/v1/jukebox/' + response.data.id,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
self.getJukeboxes()
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
clearInterval(timerId)
self.closeFormDialog()
} else {
self.step = 4
clearInterval(timerId)
}
}, 2000)
}
}
})
@ -347,15 +345,15 @@ new Vue({
}
}
},
refreshDevices() {
refreshDevices() {
self = this
self.deviceApi(
self.deviceApi(
'GET',
'https://api.spotify.com/v1/me/player/devices',
null
)
},
fetchAccessToken(code) {
fetchAccessToken(code) {
self = this
let body = 'grant_type=authorization_code'
body += '&code=' + code
@ -363,16 +361,16 @@ new Vue({
'&redirect_uri=' +
encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_id)
self.callAuthorizationApi(body)
self.callAuthorizationApi(body)
},
refreshAccessToken() {
refreshAccessToken() {
self = this
let body = 'grant_type=refresh_token'
body += '&refresh_token=' + self.jukeboxDialog.data.sp_refresh_token
body += '&client_id=' + self.jukeboxDialog.data.sp_user
self.callAuthorizationApi(body)
self.callAuthorizationApi(body)
},
callAuthorizationApi(body) {
callAuthorizationApi(body) {
self = this
console.log(
btoa(

View File

@ -6,14 +6,9 @@ new Vue({
el: '#vue',
mixins: [windowMixin],
data() {
return {
}
return {}
},
computed: {},
methods: {
},
created() {
}
methods: {},
created() {}
})

View File

@ -1,24 +1,33 @@
<q-card-section>
To use this extension you need a Spotify client ID and client secret. You
get these by creating an app in the Spotify developers dashboard
<a style="color:#43a047" href="https://developer.spotify.com/dashboard/applications">here </a>
<br /><br />Select the playlists you want people to be able to pay for,
share the frontend page, profit :) <br /><br />
Made by, <a style="color:#43a047" href="https://twitter.com/arcbtc">benarc</a>. Inspired by,
<a style="color:#43a047" href="https://twitter.com/pirosb3/status/1056263089128161280">pirosb3</a>.
To use this extension you need a Spotify client ID and client secret. You get
these by creating an app in the Spotify developers dashboard
<a
style="color: #43a047"
href="https://developer.spotify.com/dashboard/applications"
>here
</a>
<br /><br />Select the playlists you want people to be able to pay for, share
the frontend page, profit :) <br /><br />
Made by,
<a style="color: #43a047" href="https://twitter.com/arcbtc">benarc</a>.
Inspired by,
<a
style="color: #43a047"
href="https://twitter.com/pirosb3/status/1056263089128161280"
>pirosb3</a
>.
</q-card-section>
<q-expansion-item group="extras" icon="swap_vertical_circle" label="API info" :content-inset-level="0.5">
<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="List jukeboxes">
<q-card>
<q-card-section>
<code><span class="text-blue">GET</span>
/jukebox/api/v1/jukebox</code>
<code><span class="text-blue">GET</span> /jukebox/api/v1/jukebox</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>
@ -27,7 +36,8 @@
</h5>
<code>[&lt;jukebox_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X GET {{ request.url_root }}api/v1/jukebox -H "X-Api-Key: {{
<code
>curl -X GET {{ request.url_root }}api/v1/jukebox -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
@ -36,8 +46,10 @@
<q-expansion-item group="api" dense expand-separator label="Get jukebox">
<q-card>
<q-card-section>
<code><span class="text-blue">GET</span>
/jukebox/api/v1/jukebox/&lt;juke_id&gt;</code>
<code
><span class="text-blue">GET</span>
/jukebox/api/v1/jukebox/&lt;juke_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">Body (application/json)</h5>
@ -46,36 +58,44 @@
</h5>
<code>&lt;jukebox_object&gt;</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X GET {{ request.url_root }}api/v1/jukebox/&lt;juke_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}"
<code
>curl -X GET {{ request.url_root }}api/v1/jukebox/&lt;juke_id&gt; -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="Create/update track">
<q-expansion-item
group="api"
dense
expand-separator
label="Create/update track"
>
<q-card>
<q-card-section>
<code><span class="text-green">POST/PUT</span>
/jukebox/api/v1/jukebox/</code>
<code
><span class="text-green">POST/PUT</span>
/jukebox/api/v1/jukebox/</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>
<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;jukbox_object&gt;</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X POST {{ request.url_root }}api/v1/jukebox/ -d
'{"user": &lt;string, user_id&gt;,
"title": &lt;string&gt;, "wallet":&lt;string&gt;, "sp_user":
&lt;string, spotify_user_account&gt;, "sp_secret": &lt;string, spotify_user_secret&gt;, "sp_access_token":
&lt;string, not_required&gt;, "sp_refresh_token":
&lt;string, not_required&gt;, "sp_device": &lt;string, spotify_user_secret&gt;, "sp_playlists":
&lt;string, not_required&gt;, "price":
&lt;integer, not_required&gt;}' -H "Content-type:
application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}"
<code
>curl -X POST {{ request.url_root }}api/v1/jukebox/ -d '{"user":
&lt;string, user_id&gt;, "title": &lt;string&gt;,
"wallet":&lt;string&gt;, "sp_user": &lt;string,
spotify_user_account&gt;, "sp_secret": &lt;string,
spotify_user_secret&gt;, "sp_access_token": &lt;string,
not_required&gt;, "sp_refresh_token": &lt;string, not_required&gt;,
"sp_device": &lt;string, spotify_user_secret&gt;, "sp_playlists":
&lt;string, not_required&gt;, "price": &lt;integer, not_required&gt;}'
-H "Content-type: application/json" -H "X-Api-Key:
{{g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
@ -83,8 +103,10 @@
<q-expansion-item group="api" dense expand-separator label="Delete jukebox">
<q-card>
<q-card-section>
<code><span class="text-red">DELETE</span>
/jukebox/api/v1/jukebox/&lt;juke_id&gt;</code>
<code
><span class="text-red">DELETE</span>
/jukebox/api/v1/jukebox/&lt;juke_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">Body (application/json)</h5>
@ -93,9 +115,11 @@
</h5>
<code>&lt;jukebox_object&gt;</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code>curl -X DELETE {{ request.url_root }}api/v1/jukebox/&lt;juke_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}"
<code
>curl -X DELETE {{ request.url_root }}api/v1/jukebox/&lt;juke_id&gt;
-H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item></q-expansion-item
>

View File

@ -12,7 +12,9 @@
style="font-size: 20rem"
></q-icon>
<h5 class="q-my-none">Ask the host to turn on the device and launch spotify</h5>
<h5 class="q-my-none">
Ask the host to turn on the device and launch spotify
</h5>
<br />
</center>
</q-card-section>

View File

@ -4,18 +4,36 @@
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="green-7" class="q-ma-lg" @click="openNewDialog()">Add Spotify Jukebox</q-btn>
<q-btn
unelevated
color="primary"
class="q-ma-lg"
@click="openNewDialog()"
>Add Spotify Jukebox</q-btn
>
{% raw %}
<q-table flat dense :data="JukeboxLinks" row-key="id" :columns="JukeboxTable.columns"
:pagination.sync="JukeboxTable.pagination" :filter="filter">
<q-table
flat
dense
:data="JukeboxLinks"
row-key="id"
:columns="JukeboxTable.columns"
:pagination.sync="JukeboxTable.pagination"
:filter="filter"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props" auto-width>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.label }}</div>
</q-th>
@ -26,18 +44,43 @@
<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'"
@click="openQrCodeDialog(props.row.sp_id)">
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openQrCodeDialog(props.row.sp_id)"
>
<q-tooltip> Jukebox QR </q-tooltip>
</q-btn>
</q-td>
<q-td auto-width>
<q-btn flat dense size="xs" @click="updateJukebox(props.row.id)" icon="edit" color="light-blue"></q-btn>
<q-btn flat dense size="xs" @click="deleteJukebox(props.row.id)" icon="cancel" color="pink">
<q-btn
flat
dense
size="xs"
@click="updateJukebox(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
<q-btn
flat
dense
size="xs"
@click="deleteJukebox(props.row.id)"
icon="cancel"
color="pink"
>
<q-tooltip> Delete Jukebox </q-tooltip>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props" auto-width>
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.value }}</div>
</q-td>
@ -63,23 +106,62 @@
<q-dialog v-model="jukeboxDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-md q-pt-lg q-mt-md" style="width: 100%">
<q-stepper v-model="step" active-color="green-7" inactive-color="green-10" vertical animated>
<q-step :name="1" title="Pick wallet, price" icon="account_balance_wallet" :done="step > 1">
<q-input filled class="q-pt-md" dense v-model.trim="jukeboxDialog.data.title" label="Jukebox name"></q-input>
<q-select class="q-pb-md q-pt-md" filled dense emit-value v-model="jukeboxDialog.data.wallet"
:options="g.user.walletOptions" label="Wallet to use"></q-select>
<q-input filled dense v-model.trim="jukeboxDialog.data.price" type="number" max="1440" label="Price per track"
class="q-pb-lg">
<q-stepper
v-model="step"
active-color="primary"
inactive-color="secondary"
vertical
animated
>
<q-step
:name="1"
title="Pick wallet, price"
icon="account_balance_wallet"
:done="step > 1"
>
<q-input
filled
class="q-pt-md"
dense
v-model.trim="jukeboxDialog.data.title"
label="Jukebox name"
></q-input>
<q-select
class="q-pb-md q-pt-md"
filled
dense
emit-value
v-model="jukeboxDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet to use"
></q-select>
<q-input
filled
dense
v-model.trim="jukeboxDialog.data.price"
type="number"
max="1440"
label="Price per track"
class="q-pb-lg"
>
</q-input>
<div class="row">
<div class="col-4">
<q-btn
v-if="jukeboxDialog.data.title != null && jukeboxDialog.data.price != null && jukeboxDialog.data.wallet != null"
color="green-7" @click="step = 2">Continue</q-btn>
<q-btn v-else color="green-7" disable>Continue</q-btn>
color="primary"
@click="step = 2"
>Continue</q-btn
>
<q-btn v-else color="primary" disable>Continue</q-btn>
</div>
<div class="col-8">
<q-btn color="green-7" class="float-right" @click="closeFormDialog">Cancel</q-btn>
<q-btn
color="primary"
class="float-right"
@click="closeFormDialog"
>Cancel</q-btn
>
</div>
</div>
@ -90,26 +172,57 @@
<img src="/jukebox/static/spotapi.gif" />
To use this extension you need a Spotify client ID and client secret.
You get these by creating an app in the Spotify developers dashboard
<a target="_blank" style="color:#43a047" href="https://developer.spotify.com/dashboard/applications">here</a>.
<q-input filled class="q-pb-md q-pt-md" dense v-model.trim="jukeboxDialog.data.sp_user" label="Client ID">
<a
target="_blank"
style="color: #43a047"
href="https://developer.spotify.com/dashboard/applications"
>here</a
>.
<q-input
filled
class="q-pb-md q-pt-md"
dense
v-model.trim="jukeboxDialog.data.sp_user"
label="Client ID"
>
</q-input>
<q-input dense v-model="jukeboxDialog.data.sp_secret" filled :type="isPwd ? 'password' : 'text'"
label="Client secret">
<q-input
dense
v-model="jukeboxDialog.data.sp_secret"
filled
:type="isPwd ? 'password' : 'text'"
label="Client secret"
>
<template #append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer" @click="isPwd = !isPwd">
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
>
</q-icon>
</template>
</q-input>
<div class="row q-mt-md">
<div class="col-4">
<q-btn v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched"
color="green-7" @click="submitSpotifyKeys">Submit keys</q-btn>
<q-btn v-else color="green-7" disable color="green-7">Submit keys</q-btn>
<q-btn
v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched"
color="primary"
@click="submitSpotifyKeys"
>Submit keys</q-btn
>
<q-btn v-else color="primary" disable color="primary"
>Submit keys</q-btn
>
</div>
<div class="col-8">
<q-btn color="green-7" class="float-right" @click="closeFormDialog">Cancel</q-btn>
<q-btn
color="primary"
class="float-right"
@click="closeFormDialog"
>Cancel</q-btn
>
</div>
</div>
@ -120,42 +233,93 @@
<img src="/jukebox/static/spotapi1.gif" />
In the app go to edit-settings, set the redirect URI to this link
<br />
<q-btn dense outline unelevated color="green-7" size="xs"
@click="copyText(locationcb + jukeboxDialog.data.sp_id, 'Link copied to clipboard!')">{% raw %}{{ locationcb
}}{{ jukeboxDialog.data.sp_id }}{% endraw
<q-btn
dense
outline
unelevated
color="primary"
size="xs"
@click="copyText(locationcb + jukeboxDialog.data.sp_id, 'Link copied to clipboard!')"
>{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw
%}<q-tooltip> Click to copy URL </q-tooltip>
</q-btn>
<br />
Settings can be found
<a target="_blank" style="color:#43a047" href="https://developer.spotify.com/dashboard/applications">here</a>.
<a
target="_blank"
style="color: #43a047"
href="https://developer.spotify.com/dashboard/applications"
>here</a
>.
<div class="row q-mt-md">
<div class="col-4">
<q-btn v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched"
color="green-7" @click="authAccess">Authorise access</q-btn>
<q-btn v-else color="green-7" disable color="green-7">Authorise access</q-btn>
<q-btn
v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched"
color="primary"
@click="authAccess"
>Authorise access</q-btn
>
<q-btn v-else color="primary" disable color="primary"
>Authorise access</q-btn
>
</div>
<div class="col-8">
<q-btn color="green-7" class="float-right" @click="closeFormDialog">Cancel</q-btn>
<q-btn
color="primary"
class="float-right"
@click="closeFormDialog"
>Cancel</q-btn
>
</div>
</div>
<br />
</q-step>
<q-step :name="4" title="Select playlists" icon="queue_music" active-color="green-8" :done="step > 4">
<q-select class="q-pb-md q-pt-md" filled dense emit-value v-model="jukeboxDialog.data.sp_device"
:options="devices" label="Device jukebox will play to"></q-select>
<q-select class="q-pb-md" filled dense multiple emit-value v-model="jukeboxDialog.data.sp_playlists"
:options="playlists" label="Playlists available to the jukebox"></q-select>
<q-step
:name="4"
title="Select playlists"
icon="queue_music"
active-color="primary"
:done="step > 4"
>
<q-select
class="q-pb-md q-pt-md"
filled
dense
emit-value
v-model="jukeboxDialog.data.sp_device"
:options="devices"
label="Device jukebox will play to"
></q-select>
<q-select
class="q-pb-md"
filled
dense
multiple
emit-value
v-model="jukeboxDialog.data.sp_playlists"
:options="playlists"
label="Playlists available to the jukebox"
></q-select>
<div class="row q-mt-md">
<div class="col-5">
<q-btn v-if="jukeboxDialog.data.sp_device != null && jukeboxDialog.data.sp_playlists != null"
color="green-7" @click="createJukebox">Create Jukebox</q-btn>
<q-btn v-else color="green-7" disable>Create Jukebox</q-btn>
<q-btn
v-if="jukeboxDialog.data.sp_device != null && jukeboxDialog.data.sp_playlists != null"
color="primary"
@click="createJukebox"
>Create Jukebox</q-btn
>
<q-btn v-else color="primary" disable>Create Jukebox</q-btn>
</div>
<div class="col-7">
<q-btn color="green-7" class="float-right" @click="closeFormDialog">Cancel</q-btn>
<q-btn
color="primary"
class="float-right"
@click="closeFormDialog"
>Cancel</q-btn
>
</div>
</div>
</q-step>
@ -169,15 +333,28 @@
<h5 class="q-my-none">Shareable Jukebox QR</h5>
</center>
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode :value="qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id" :options="{width: 800}"
class="rounded-borders"></qrcode>
<qrcode
:value="qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
<div class="row q-mt-lg q-gutter-sm">
<q-btn outline color="grey"
@click="copyText(qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id, 'Link copied to clipboard!')">
Copy jukebox link</q-btn>
<q-btn outline color="grey" type="a" :href="qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id"
target="_blank">Open jukebox</q-btn>
<q-btn
outline
color="grey"
@click="copyText(qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id, 'Link copied to clipboard!')"
>
Copy jukebox link</q-btn
>
<q-btn
outline
color="grey"
type="a"
:href="qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id"
target="_blank"
>Open jukebox</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
@ -186,4 +363,4 @@
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="https://cdn.jsdelivr.net/npm/pica@6.1.1/dist/pica.min.js"></script>
<script src="/jukebox/static/js/index.js"></script>
{% endblock %}
{% endblock %}

View File

@ -199,6 +199,7 @@
self.receive.paymentReq = response.data[0][1]
self.receive.paymentHash = response.data[0][0]
self.receive.dialogues.second = true
self.$q.notify({
message: 'Processing'
})

View File

@ -4,7 +4,7 @@ async def m001_initial(db):
"""
await db.execute(
"""
CREATE TABLE livestreams (
CREATE TABLE IF NOT EXISTS livestreams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet TEXT NOT NULL,
fee_pct INTEGER NOT NULL DEFAULT 10,
@ -15,7 +15,7 @@ async def m001_initial(db):
await db.execute(
"""
CREATE TABLE producers (
CREATE TABLE IF NOT EXISTS producers (
livestream INTEGER NOT NULL REFERENCES livestreams (id),
id INTEGER PRIMARY KEY AUTOINCREMENT,
user TEXT NOT NULL,
@ -27,7 +27,7 @@ async def m001_initial(db):
await db.execute(
"""
CREATE TABLE tracks (
CREATE TABLE IF NOT EXISTS tracks (
livestream INTEGER NOT NULL REFERENCES livestreams (id),
id INTEGER PRIMARY KEY AUTOINCREMENT,
download_url TEXT,

View File

@ -1,5 +1,5 @@
import json
import trio # type: ignore
import trio
from lnbits.core.models import Payment
from lnbits.core.crud import create_payment

View File

@ -26,7 +26,7 @@
</div>
<div class="col">
{% raw %}
<q-btn unelevated color="deep-purple" type="submit">
<q-btn unelevated color="primary" type="submit">
{{ nextCurrentTrack && nextCurrentTrack ===
livestream.current_track ? 'Stop' : 'Set' }} current track
</q-btn>
@ -46,7 +46,7 @@
></q-input>
</div>
<div class="col">
<q-btn unelevated color="deep-purple" type="submit"
<q-btn unelevated color="primary" type="submit"
>Set percent rate</q-btn
>
</div>
@ -61,7 +61,7 @@
<h5 class="text-subtitle1 q-my-none">Tracks</h5>
</div>
<div class="col q-ml-lg">
<q-btn unelevated color="deep-purple" @click="openAddTrackDialog"
<q-btn unelevated color="primary" @click="openAddTrackDialog"
>Add new track</q-btn
>
</div>
@ -296,7 +296,7 @@
<div class="col q-ml-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="disabledAddTrackButton()"
type="submit"
>

View File

@ -1,6 +1,6 @@
{
"name": "LndHub",
"short_description": "Access lnbits from BlueWallet or Zeus.",
"short_description": "Access lnbits from BlueWallet or Zeus",
"icon": "navigation",
"contributors": ["fiatjaf"]
}

View File

@ -10,3 +10,8 @@ lnticket_ext: Blueprint = Blueprint(
from .views_api import * # noqa
from .views import * # noqa
from .tasks import register_listeners
from lnbits.tasks import record_async
lnticket_ext.record(record_async(register_listeners))

View File

@ -0,0 +1,37 @@
import json
import trio # type: ignore
from lnbits.core.models import Payment
from lnbits.core.crud import create_payment
from lnbits.core import db as core_db
from lnbits.tasks import register_invoice_listener, internal_invoice_paid
from lnbits.helpers import urlsafe_short_hash
from .crud import get_ticket, set_ticket_paid
async def register_listeners():
invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2)
register_invoice_listener(invoice_paid_chan_send)
await wait_for_paid_invoices(invoice_paid_chan_recv)
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
async for payment in invoice_paid_chan:
await on_invoice_paid(payment)
async def on_invoice_paid(payment: Payment) -> None:
if "lnticket" != payment.extra.get("tag"):
# not a lnticket invoice
return
ticket = await get_ticket(payment.checking_id)
if not ticket:
print("this should never happen", payment)
return
await payment.set_pending(False)
await set_ticket_paid(payment.payment_hash)
_ticket = await get_ticket(payment.checking_id)
print("ticket", _ticket)

View File

@ -33,7 +33,7 @@
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="formDialog.data.name == '' || formDialog.data.text == ''"
type="submit"
>Submit</q-btn
@ -77,7 +77,7 @@
{% endblock %} {% block scripts %}
<script>
console.log('{{ form_costpword }}')
//console.log('{{ form_costpword }}')
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
@ -99,7 +99,11 @@
show: false,
status: 'pending',
paymentReq: null
}
},
wallet: {
inkey: ''
},
cancelListener: () => {}
}
},
computed: {
@ -128,12 +132,35 @@
},
closeReceiveDialog: function () {
var checker = this.receive.paymentChecker
var checker = this.startPaymentNotifier
dismissMsg()
clearInterval(paymentChecker)
setTimeout(function () {}, 10000)
},
startPaymentNotifier() {
this.cancelListener()
this.cancelListener = LNbits.events.onInvoicePaid(
this.wallet,
payment => {
this.receive = {
show: false,
status: 'complete',
paymentReq: null
}
dismissMsg()
this.formDialog.data.name = ''
this.formDialog.data.email = ''
this.formDialog.data.text = ''
this.$q.notify({
type: 'positive',
message: 'Sent, thank you!',
icon: 'thumb_up'
})
}
)
},
Invoice: function () {
var self = this
axios
@ -158,39 +185,15 @@
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)
self.receive = {
show: false,
status: 'complete',
paymentReq: null
}
dismissMsg()
self.formDialog.data.name = ''
self.formDialog.data.email = ''
self.formDialog.data.text = ''
self.$q.notify({
type: 'positive',
message: 'Sent, thank you!',
icon: 'thumb_up'
})
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}, 2000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
},
created() {
this.wallet.inkey = '{{form_wallet}}'
this.startPaymentNotifier()
}
})
</script>

View File

@ -4,7 +4,7 @@
<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"
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New Form</q-btn
>
</q-card-section>
@ -90,6 +90,16 @@
<div class="col">
<h5 class="text-subtitle1 q-my-none">Tickets</h5>
</div>
<!-- <div class="col-auto">
<q-btn
flat
color="grey"
icon="autorenew"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="getTickets"
><q-tooltip> Refresh Tickets </q-tooltip></q-btn
>
</div> -->
<div class="col-auto">
<q-btn flat color="grey" @click="exportticketsCSV"
>Export to CSV</q-btn
@ -207,7 +217,7 @@
<q-btn
v-if="formDialog.data.id"
unelevated
color="deep-purple"
color="primary"
type="submit"
>Update Form</q-btn
>
@ -215,7 +225,7 @@
<q-btn
v-else
unelevated
color="deep-purple"
color="primary"
:disable="formDialog.data.costpword == null || formDialog.data.costpword < 0 || formDialog.data.name == null"
type="submit"
>Create Form</q-btn
@ -230,7 +240,7 @@
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapLNTicket = function(obj) {
const mapLNTicket = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
@ -243,7 +253,7 @@
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function() {
data: function () {
return {
forms: [],
tickets: [],
@ -290,11 +300,12 @@
formDialog: {
show: false,
data: {}
}
},
cancelListener: () => {}
}
},
methods: {
getTickets: function() {
getTickets: function () {
var self = this
LNbits.api
@ -303,40 +314,43 @@
'/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)
})
.then(function (response) {
self.tickets = response.data
.map(function (obj) {
if (!obj?.paid) return
return mapLNTicket(obj)
})
.filter(v => v)
})
},
deleteTicket: function(ticketId) {
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() {
.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) {
.then(function (response) {
self.tickets = _.reject(self.tickets, function (obj) {
return obj.id == ticketId
})
})
.catch(function(error) {
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportticketsCSV: function() {
exportticketsCSV: function () {
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
},
getForms: function() {
getForms: function () {
var self = this
LNbits.api
@ -345,16 +359,17 @@
'/lnticket/api/v1/forms?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function(response) {
self.forms = response.data.map(function(obj) {
.then(function (response) {
self.forms = response.data.map(function (obj) {
return mapLNTicket(obj)
})
})
},
sendFormData: function() {
sendFormData: function () {
var wallet = _.findWhere(this.g.user.wallets, {
id: this.formDialog.data.wallet
})
this.formDialog.data.inkey = wallet.inkey
var data = this.formDialog.data
if (data.id) {
@ -364,22 +379,23 @@
}
},
createForm: function(wallet, data) {
createForm: function (wallet, data) {
var self = this
console.log('create', data)
LNbits.api
.request('POST', '/lnticket/api/v1/forms', wallet.inkey, data)
.then(function(response) {
.then(function (response) {
self.forms.push(mapLNTicket(response.data))
self.formDialog.show = false
self.formDialog.data = {}
})
.catch(function(error) {
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
updateformDialog: function(formId) {
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
@ -387,10 +403,9 @@
this.formDialog.data.costpword = link.costpword
this.formDialog.show = true
},
updateForm: function(wallet, data) {
updateForm: function (wallet, data) {
var self = this
console.log(data)
console.log('update', data)
LNbits.api
.request(
'PUT',
@ -398,50 +413,67 @@
wallet.inkey,
data
)
.then(function(response) {
self.forms = _.reject(self.forms, function(obj) {
.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) {
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteForm: function(formsId) {
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() {
.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) {
.then(function (response) {
self.forms = _.reject(self.forms, function (obj) {
return obj.id == formsId
})
})
.catch(function(error) {
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportformsCSV: function() {
exportformsCSV: function () {
LNbits.utils.exportCSV(this.formsTable.columns, this.forms)
},
startPaymentNotifier() {
this.cancelListener()
this.cancelListener = LNbits.events.onInvoicePaid(
this.g.user.wallets[0],
payment => {
this.getTickets()
this.$q.notify({
type: 'positive',
message: 'New ticket arrived!',
icon: 'textsms'
})
}
)
}
},
created: function() {
created: function () {
if (this.g.user.wallets.length) {
this.getTickets()
this.getForms()
this.startPaymentNotifier()
}
}
})

View File

@ -1,5 +1,6 @@
from quart import g, abort, render_template
from lnbits.core.crud import get_wallet
from lnbits.decorators import check_user_exists, validate_uuids
from http import HTTPStatus
@ -20,10 +21,13 @@ async def display(form_id):
if not form:
abort(HTTPStatus.NOT_FOUND, "LNTicket does not exist.")
wallet = await get_wallet(form.wallet)
return await render_template(
"lnticket/display.html",
form_id=form.id,
form_name=form.name,
form_desc=form.description,
form_costpword=form.costpword,
form_wallet=wallet.inkey,
)

View File

@ -174,7 +174,7 @@ new Vue({
LNbits.utils.notifyApiError(err)
})
},
deletePayLink: linkId => {
deletePayLink(linkId) {
var link = _.findWhere(this.payLinks, {id: linkId})
LNbits.utils
@ -215,7 +215,6 @@ new Vue({
getPayLinks()
}, 20000)
}
LNbits.api
.request('GET', '/lnurlp/api/v1/currencies')
.then(response => {

View File

@ -1,4 +1,4 @@
import trio # type: ignore
import trio
import json
import httpx

View File

@ -7,7 +7,7 @@
<q-expansion-item group="api" dense expand-separator label="List pay links">
<q-card>
<q-card-section>
<code><span class="text-blue">GET</span> /api/v1/links</code>
<code><span class="text-blue">GET</span> /lnurlp/api/v1/links</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>
@ -27,7 +27,7 @@
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span> /api/v1/links/&lt;pay_id&gt;</code
><span class="text-blue">GET</span> /lnurlp/api/v1/links/&lt;pay_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
@ -52,11 +52,11 @@
>
<q-card>
<q-card-section>
<code><span class="text-green">POST</span> /api/v1/links</code>
<code><span class="text-green">POST</span> /lnurlp/api/v1/links</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>{"description": &lt;string&gt; "amount": &lt;integer&gt;}</code>
<code>{"description": &lt;string&gt; "amount": &lt;integer&gt; "max": &lt;integer&gt; "min": &lt;integer&gt; "comment_chars": &lt;integer&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
@ -64,7 +64,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}api/v1/links -d '{"description":
&lt;string&gt;, "amount": &lt;integer&gt;}' -H "Content-type:
&lt;string&gt;, "amount": &lt;integer&gt;, "max": &lt;integer&gt;, "min": &lt;integer&gt;, "comment_chars": &lt;integer&gt;}' -H "Content-type:
application/json" -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
@ -80,7 +80,7 @@
<q-card-section>
<code
><span class="text-green">PUT</span>
/api/v1/links/&lt;pay_id&gt;</code
/lnurlp/api/v1/links/&lt;pay_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 />
@ -111,7 +111,7 @@
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/api/v1/links/&lt;pay_id&gt;</code
/lnurlp/api/v1/links/&lt;pay_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 />

View File

@ -4,7 +4,7 @@
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New pay link</q-btn
>
</q-card-section>
@ -227,14 +227,14 @@
<q-btn
v-if="formDialog.data.id"
unelevated
color="deep-purple"
color="primary"
type="submit"
>Update pay link</q-btn
>
<q-btn
v-else
unelevated
color="deep-purple"
color="primary"
:disable="
formDialog.data.wallet == null ||
formDialog.data.description == null ||

View File

@ -4,7 +4,7 @@ async def m001_initial(db):
"""
await db.execute(
"""
CREATE TABLE shops (
CREATE TABLE IF NOT EXISTS shops (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet TEXT NOT NULL,
method TEXT NOT NULL,
@ -15,7 +15,7 @@ async def m001_initial(db):
await db.execute(
"""
CREATE TABLE items (
CREATE TABLE IF NOT EXISTS items (
shop INTEGER NOT NULL REFERENCES shop (id),
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,

View File

@ -9,7 +9,7 @@
<h5 class="text-subtitle1 q-my-none">Items</h5>
</div>
<div class="col q-ml-lg">
<q-btn unelevated color="deep-purple" @click="openNewDialog()"
<q-btn unelevated color="primary" @click="openNewDialog()"
>Add new item</q-btn
>
</div>
@ -111,7 +111,7 @@
<q-btn
type="a"
outline
color="purple"
color="secondary"
:href="'print?items=' + printItems.map(({id}) => id).join(',')"
>Print QR Codes</q-btn
>
@ -123,7 +123,7 @@
<q-tabs
v-model="confirmationMethod"
no-caps
class="bg-purple text-white shadow-2"
class="bg-dark text-white shadow-2"
>
<q-tab name="wordlist" label="Wordlist"></q-tab>
<q-tab name="totp" label="TOTP (Google Authenticator)"></q-tab>
@ -151,7 +151,7 @@
>
<q-btn
unelevated
color="deep-purple"
color="primary"
type="submit"
:disabled="!wordlistTainted"
>
@ -180,7 +180,7 @@
>
<q-btn
unelevated
color="deep-purple"
color="primary"
:disabled="offlineshop.method === 'totp'"
@click="setMethod"
>
@ -199,7 +199,7 @@
<q-btn
unelevated
color="deep-purple"
color="primary"
:disabled="offlineshop.method === 'none'"
@click="setMethod"
>
@ -308,7 +308,7 @@
<div class="col q-ml-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="disabledAddItemButton()"
type="submit"
>

View File

@ -24,7 +24,7 @@
dense
flat
icon="check"
color="deep-purple"
color="primary"
type="submit"
@click="createInvoice"
:disabled="userAmount < paywallAmount || paymentReq"

View File

@ -4,7 +4,7 @@
<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"
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New paywall</q-btn
>
</q-card-section>
@ -141,7 +141,7 @@
<q-item-section avatar>
<q-checkbox
v-model="formDialog.data.remembers"
color="deep-purple"
color="primary"
></q-checkbox>
</q-item-section>
<q-item-section>
@ -157,7 +157,7 @@
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="formDialog.data.amount == null || formDialog.data.amount < 0 || formDialog.data.url == null || formDialog.data.memo == null"
type="submit"
>Create paywall</q-btn

View File

@ -5,10 +5,7 @@
<q-card>
<q-card-section>
{% raw %}
<q-btn
unelevated
color="deep-purple"
@click="formDialogCharge.show = true"
<q-btn unelevated color="primary" @click="formDialogCharge.show = true"
>New charge
</q-btn>
</q-card-section>
@ -265,7 +262,7 @@
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="
formDialogCharge.data.time == null ||
formDialogCharge.data.amount == null"

View File

@ -1,6 +1,33 @@
# Split Payments
Set this and forget. It will keep splitting your payments across wallets forever.
## Have payments split between multiple wallets
LNBits Split Payments extension allows for distributing payments across multiple wallets. Set it and forget it. It will keep splitting your payments across wallets forever.
## Usage
1. After enabling the extension, choose the source wallet that will receive and distribute the Payments
![choose wallet](https://i.imgur.com/nPQudqL.png)
2. Add the wallet or wallets info to split payments to
![split wallets](https://i.imgur.com/5hCNWpg.png) - get the wallet id, or an invoice key from a different wallet. It can be a completely different user as long as it's under the same LNbits instance/domain. You can get the wallet information on the API Info section on every wallet page\
![wallet info](https://i.imgur.com/betqflC.png) - set a wallet _Alias_ for your own identification\
- set how much, in percentage, this wallet will receive from every payment sent to the source wallets
3. When done, click "SAVE TARGETS" to make the splits effective
4. You can have several wallets to split to, as long as the sum of the percentages is under or equal to 100%
5. When the source wallet receives a payment, the extension will automatically split the corresponding values to every wallet\
- on receiving a 20 sats payment\
![get 20 sats payment](https://i.imgur.com/BKp0xvy.png)
- source wallet gets 18 sats\
![source wallet](https://i.imgur.com/GCxDZ5s.png)
- Ben's wallet (the wallet from the example) instantly, and feeless, gets the corresponding 10%, or 2 sats\
![ben wallet](https://i.imgur.com/MfsccNa.png)
## Sponsored by

View File

@ -1,6 +1,6 @@
{
"name": "SplitPayments",
"short_description": "Split incoming payments to other wallets.",
"short_description": "Split incoming payments across wallets",
"icon": "call_split",
"contributors": [
"fiatjaf",

View File

@ -4,7 +4,7 @@ async def m001_initial(db):
"""
await db.execute(
"""
CREATE TABLE targets (
CREATE TABLE IF NOT EXISTS targets (
wallet TEXT NOT NULL,
source TEXT NOT NULL,
percent INTEGER NOT NULL CHECK (percent >= 0 AND percent <= 100),

View File

@ -1,5 +1,5 @@
import json
import trio # type: ignore
import trio
from lnbits.core.models import Payment
from lnbits.core.crud import create_payment

View File

@ -60,7 +60,7 @@
<q-row class="row justify-evenly q-pa-lg">
<q-col>
<q-btn unelevated outline color="purple" @click="clearTargets">
<q-btn unelevated outline color="secondary" @click="clearTargets">
Clear
</q-btn>
</q-col>
@ -68,7 +68,7 @@
<q-col>
<q-btn
unelevated
color="deep-purple"
color="primary"
type="submit"
:disabled="!isDirty"
>

View File

@ -1,6 +1,6 @@
from http import HTTPStatus
from quart.json import jsonify
import trio # type: ignore
import trio
import httpx
from .crud import get_domain, set_subdomain_paid

View File

@ -11,8 +11,11 @@
</h5>
<p>
Charge people for using your subdomain name...<br />
Are you the owner of <b>cool-domain.com</b> and want to sell
<i>cool-subdomain</i>.<b>cool-domain.com</b>
<a
href="https://github.com/lnbits/lnbits/tree/master/lnbits/extensions/subdomains"
>More details</a
>
<br />
<small>
Created by, <a href="https://github.com/grmkris">Kris</a></small

View File

@ -53,7 +53,7 @@
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="formDialog.data.subdomain == '' || formDialog.data.ip == '' || formDialog.data.duration == ''"
type="submit"
>Submit</q-btn

View File

@ -5,7 +5,7 @@
<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="domainDialog.show = true"
<q-btn unelevated color="primary" @click="domainDialog.show = true"
>New Domain</q-btn
>
</q-card-section>
@ -149,17 +149,17 @@
</q-table>
</q-card-section>
</q-card>
<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 Subdomain extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "subdomains/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
</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 Subdomain extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "subdomains/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="domainDialog.show" position="top">
@ -233,14 +233,14 @@
<q-btn
v-if="domainDialog.data.id"
unelevated
color="deep-purple"
color="primary"
type="submit"
>Update Form</q-btn
>
<q-btn
v-else
unelevated
color="deep-purple"
color="primary"
:disable="domainDialog.data.cost == null || domainDialog.data.cost < 0 || domainDialog.data.domain == null"
type="submit"
>Create Domain</q-btn

View File

@ -4,7 +4,7 @@
<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"
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New TPoS</q-btn
>
</q-card-section>
@ -119,7 +119,7 @@
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="formDialog.data.currency == null || formDialog.data.name == null"
type="submit"
>Create TPoS</q-btn

View File

@ -4,10 +4,10 @@
<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"
<q-btn unelevated color="primary" @click="userDialog.show = true"
>New User</q-btn
>
<q-btn unelevated color="deep-purple" @click="walletDialog.show = true"
<q-btn unelevated color="primary" @click="walletDialog.show = true"
>New Wallet
</q-btn>
</q-card-section>
@ -172,7 +172,7 @@
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="userDialog.data.walname == null"
type="submit"
>Create User</q-btn
@ -202,7 +202,7 @@
></q-input>
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="walletDialog.data.walname == null"
type="submit"
>Create Wallet</q-btn
@ -214,7 +214,7 @@
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapUserManager = function(obj) {
var mapUserManager = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
@ -228,7 +228,7 @@
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function() {
data: function () {
return {
wallets: [],
users: [],
@ -277,8 +277,8 @@
}
},
computed: {
userOptions: function() {
return this.users.map(function(obj) {
userOptions: function () {
return this.users.map(function (obj) {
console.log(obj.id)
return {
value: String(obj.id),
@ -290,7 +290,7 @@
methods: {
///////////////Users////////////////////////////
getUsers: function() {
getUsers: function () {
var self = this
LNbits.api
@ -299,20 +299,20 @@
'/usermanager/api/v1/users',
this.g.user.wallets[0].inkey
)
.then(function(response) {
self.users = response.data.map(function(obj) {
.then(function (response) {
self.users = response.data.map(function (obj) {
return mapUserManager(obj)
})
})
},
openUserUpdateDialog: function(linkId) {
openUserUpdateDialog: function (linkId) {
var link = _.findWhere(this.users, {id: linkId})
this.userDialog.data = _.clone(link._data)
this.userDialog.show = true
},
sendUserFormData: function() {
sendUserFormData: function () {
if (this.userDialog.data.id) {
} else {
var data = {
@ -329,7 +329,7 @@
}
},
createUser: function(data) {
createUser: function (data) {
var self = this
LNbits.api
.request(
@ -338,48 +338,48 @@
this.g.user.wallets[0].inkey,
data
)
.then(function(response) {
.then(function (response) {
self.users.push(mapUserManager(response.data))
self.userDialog.show = false
self.userDialog.data = {}
data = {}
self.getWallets()
})
.catch(function(error) {
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteUser: function(userId) {
deleteUser: function (userId) {
var self = this
console.log(userId)
LNbits.utils
.confirmDialog('Are you sure you want to delete this User link?')
.onOk(function() {
.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) {
.then(function (response) {
self.users = _.reject(self.users, function (obj) {
return obj.id == userId
})
})
.catch(function(error) {
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportUsersCSV: function() {
exportUsersCSV: function () {
LNbits.utils.exportCSV(this.usersTable.columns, this.users)
},
///////////////Wallets////////////////////////////
getWallets: function() {
getWallets: function () {
var self = this
LNbits.api
@ -388,19 +388,19 @@
'/usermanager/api/v1/wallets',
this.g.user.wallets[0].inkey
)
.then(function(response) {
self.wallets = response.data.map(function(obj) {
.then(function (response) {
self.wallets = response.data.map(function (obj) {
return mapUserManager(obj)
})
})
},
openWalletUpdateDialog: function(linkId) {
openWalletUpdateDialog: function (linkId) {
var link = _.findWhere(this.users, {id: linkId})
this.walletDialog.data = _.clone(link._data)
this.walletDialog.show = true
},
sendWalletFormData: function() {
sendWalletFormData: function () {
if (this.walletDialog.data.id) {
} else {
var data = {
@ -415,7 +415,7 @@
}
},
createWallet: function(data) {
createWallet: function (data) {
var self = this
LNbits.api
.request(
@ -424,43 +424,43 @@
this.g.user.wallets[0].inkey,
data
)
.then(function(response) {
.then(function (response) {
self.wallets.push(mapUserManager(response.data))
self.walletDialog.show = false
self.walletDialog.data = {}
data = {}
})
.catch(function(error) {
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteWallet: function(userId) {
deleteWallet: function (userId) {
var self = this
LNbits.utils
.confirmDialog('Are you sure you want to delete this wallet link?')
.onOk(function() {
.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) {
.then(function (response) {
self.wallets = _.reject(self.wallets, function (obj) {
return obj.id == userId
})
})
.catch(function(error) {
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportWalletsCSV: function() {
exportWalletsCSV: function () {
LNbits.utils.exportCSV(this.walletsTable.columns, this.wallets)
}
},
created: function() {
created: function () {
if (this.g.user.wallets.length) {
this.getUsers()
this.getWallets()

View File

@ -5,10 +5,10 @@
<q-card>
<q-card-section>
{% raw %}
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New wallet
</q-btn>
<q-btn unelevated color="deep-purple" icon="edit">
<q-btn unelevated color="primary" icon="edit">
<div class="cursor-pointer">
<q-tooltip> Point to another Mempool </q-tooltip>
{{ this.mempool.endpoint }}
@ -143,7 +143,7 @@
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="
formDialog.data.masterpub == null ||
formDialog.data.title == null"

View File

@ -99,18 +99,19 @@ async def api_lnurl_callback(unique_hash):
)
try:
await pay_invoice(
wallet_id=link.wallet,
payment_request=payment_request,
max_sat=link.max_withdrawable,
extra={"tag": "withdraw"},
)
usescsv = ""
for x in range(1, link.uses - link.used):
usecv = link.usescsv.split(",")
usescsv += "," + str(usecv[x])
usecsvback = usescsv
usescsv = usescsv[1:]
changesback = {
"open_time": link.wait_time,
"used": link.used,
"usescsv": usecsvback,
}
changes = {
"open_time": link.wait_time + now,
"used": link.used + 1,
@ -118,11 +119,21 @@ async def api_lnurl_callback(unique_hash):
}
await update_withdraw_link(link.id, **changes)
await pay_invoice(
wallet_id=link.wallet,
payment_request=payment_request,
max_sat=link.max_withdrawable,
extra={"tag": "withdraw"},
)
except ValueError as e:
await update_withdraw_link(link.id, **changesback)
return jsonify({"status": "ERROR", "reason": str(e)})
except PermissionError:
await update_withdraw_link(link.id, **changesback)
return jsonify({"status": "ERROR", "reason": "Withdraw link is empty."})
except Exception as e:
await update_withdraw_link(link.id, **changesback)
return jsonify({"status": "ERROR", "reason": str(e)})
return jsonify({"status": "OK"}), HTTPStatus.OK

View File

@ -7,7 +7,7 @@
{% if link.is_spent %}
<q-badge color="red" class="q-mb-md">Withdraw is spent.</q-badge>
{% endif %}
<a href="{{ link.lnurl }}">
<a href="lightning://{{ link.lnurl }}">
<q-responsive :ratio="1" class="q-mx-md">
<qrcode
:value="this.here + '/?lightning={{link.lnurl }}'"

View File

@ -6,13 +6,10 @@
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn
unelevated
color="deep-purple"
@click="simpleformDialog.show = true"
<q-btn unelevated color="primary" @click="simpleformDialog.show = true"
>Quick vouchers</q-btn
>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
<q-btn unelevated color="primary" @click="formDialog.show = true"
>Advanced withdraw link(s)</q-btn
>
</q-card-section>
@ -197,7 +194,7 @@
<q-item-section avatar>
<q-checkbox
v-model="formDialog.data.is_unique"
color="deep-purple"
color="primary"
></q-checkbox>
</q-item-section>
<q-item-section>
@ -216,14 +213,14 @@
<q-btn
v-if="formDialog.data.id"
unelevated
color="deep-purple"
color="primary"
type="submit"
>Update withdraw link</q-btn
>
<q-btn
v-else
unelevated
color="deep-purple"
color="primary"
:disable="
formDialog.data.wallet == null ||
formDialog.data.title == null ||
@ -281,7 +278,7 @@
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="
simpleformDialog.data.wallet == null ||
@ -324,7 +321,7 @@
qrCodeDialog.data.uses }}
<q-linear-progress
:value="qrCodeDialog.data.used / qrCodeDialog.data.uses"
color="deep-purple"
color="primary"
class="q-mt-sm"
></q-linear-progress>
</p>

View File

@ -29,7 +29,9 @@ LNBITS_ALLOWED_USERS: List[str] = env.list(
LNBITS_DISABLED_EXTENSIONS: List[str] = env.list(
"LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str
)
LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
LNBITS_THEME_OPTIONS = env.str("LNBITS_THEME_OPTIONS", default="classic,green,orange")
WALLET = wallet_class()
DEFAULT_WALLET_NAME = env.str("LNBITS_DEFAULT_WALLET_NAME", default="LNbits wallet")

View File

@ -14,11 +14,18 @@ window.LNbits = {
data: data
})
},
createInvoice: function (wallet, amount, memo, lnurlCallback = null) {
createInvoice: async function (
wallet,
amount,
memo,
unit = 'sat',
lnurlCallback = null
) {
return this.request('post', '/api/v1/payments', wallet.inkey, {
out: false,
amount: amount,
memo: memo,
unit: unit,
lnurl_callback: lnurlCallback
})
},
@ -300,11 +307,17 @@ window.windowMixin = {
extensions: [],
user: null,
wallet: null,
payments: []
payments: [],
allowedThemes: null
}
}
},
methods: {
changeColor: function (newValue) {
document.body.setAttribute('data-theme', newValue)
this.$q.localStorage.set('lnbits.theme', newValue)
},
toggleDarkMode: function () {
this.$q.dark.toggle()
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive)
@ -321,6 +334,13 @@ window.windowMixin = {
},
created: function () {
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode'))
this.g.allowedThemes = window.allowedThemes
if (this.$q.localStorage.getItem('lnbits.theme')) {
document.body.setAttribute(
'data-theme',
this.$q.localStorage.getItem('lnbits.theme')
)
}
if (window.user) {
this.g.user = Object.freeze(window.LNbits.map.user(window.user))
}

View File

@ -35,7 +35,7 @@ Vue.component('lnbits-wallet-list', {
<q-item-section side>
<q-avatar size="md"
:color="(activeWallet && activeWallet.id === wallet.id)
? (($q.dark.isActive) ? 'deep-purple-5' : 'deep-purple')
? (($q.dark.isActive) ? 'primary' : 'primary')
: 'grey-5'">
<q-icon name="flash_on" :size="($q.dark.isActive) ? '21px' : '20px'"
:color="($q.dark.isActive) ? 'blue-grey-10' : 'grey-3'"></q-icon>
@ -118,7 +118,7 @@ Vue.component('lnbits-extension-list', {
<q-item-section side>
<q-avatar size="md"
:color="(extension.isActive)
? (($q.dark.isActive) ? 'deep-purple-5' : 'deep-purple')
? (($q.dark.isActive) ? 'primary' : 'primary')
: 'grey-5'">
<q-icon :name="extension.icon" :size="($q.dark.isActive) ? '21px' : '20px'"
:color="($q.dark.isActive) ? 'blue-grey-10' : 'grey-3'"></q-icon>
@ -219,7 +219,7 @@ Vue.component('lnbits-payment-details', {
</div>
<div class="row" v-for="entry in extras">
<div class="col-3">
<q-badge v-if="hasTag" color="purple" text-color="white">
<q-badge v-if="hasTag" color="secondary" text-color="white">
extra
</q-badge>
<b>{{ entry.key }}</b>:

View File

@ -1,34 +1,97 @@
$dark-background: #1f2234;
$dark-card-background: #333646;
$themes: (
'classic': (
primary: #673ab7,
secondary: #9c27b0,
dark: #1f2234,
info: #333646,
marginal-bg: #1f2234,
marginal-text: #fff
),
'mint': (
primary: #3ab77d,
secondary: #27b065,
dark: #1f342b,
info: #334642,
marginal-bg: #1f342b,
marginal-text: #fff
),
'autumn': (
primary: #b7763a,
secondary: #b07927,
dark: #34291f,
info: #463f33,
marginal-bg: #342a1f,
marginal-text: rgb(255, 255, 255)
),
'flamingo': (
primary: #d11d53,
secondary: #db3e6d,
dark: #803a45,
info: #ec7599,
marginal-bg: #803a45,
marginal-text: rgb(255, 255, 255)
),
'monochrome': (
primary: #494949,
secondary: #6b6b6b,
dark: #000,
info: rgb(39, 39, 39),
marginal-bg: #000,
marginal-text: rgb(255, 255, 255)
)
);
@each $theme, $colors in $themes {
@each $name, $color in $colors {
@if $name == 'dark' {
[data-theme='#{$theme}'] .q-drawer--dark,
body[data-theme='#{$theme}'].body--dark,
[data-theme='#{$theme}'] .q-menu--dark {
background: $color !important;
}
}
@if $name == 'info' {
[data-theme='#{$theme}'] .q-card--dark,
[data-theme='#{$theme}'] .q-stepper--dark {
background: $color !important;
}
}
}
[data-theme='#{$theme}'] {
@each $name, $color in $colors {
.bg-#{$name} {
background: $color !important;
}
.text-#{$name} {
color: $color !important;
}
}
}
}
[data-theme='salvador'] .q-drawer--dark {
background: #242424 !important;
}
[data-theme='salvador'] .q-header {
background: #0f47af !important;
}
[data-theme='flamingo'] .q-drawer--dark {
background: #e75480 !important;
}
[data-theme='flamingo'] .q-header {
background: #e75480 !important;
}
[v-cloak] {
display: none;
}
.bg-lnbits-dark {
background-color: $dark-background;
}
body.body--dark,
body.body--dark .q-drawer--dark,
body.body--dark .q-menu--dark {
background: $dark-background;
}
body.body--dark .q-card--dark {
background: $dark-card-background;
}
body.body--dark .q-table--dark {
background: transparent;
}
body.body--light,
body.body--light .q-drawer {
background: whitesmoke;
}
body.body--dark .q-field--error {
.text-negative,
.q-field__messages {

View File

@ -1,5 +1,5 @@
import time
import trio # type: ignore
import trio
from http import HTTPStatus
from typing import Optional, List, Callable
from quart_trio import QuartTrio

View File

@ -7,6 +7,7 @@
{% endfor %}
<!---->
<link rel="stylesheet" type="text/css" href="/static/css/base.css" />
{% block styles %}{% endblock %}
<title>{% block title %}{{ SITE_TITLE }}{% endblock %}</title>
<meta charset="utf-8" />
@ -19,9 +20,9 @@
{% block head_scripts %}{% endblock %}
</head>
<body>
<body data-theme="classic">
<q-layout id="vue" view="hHh lpR lfr" v-cloak>
<q-header bordered class="bg-lnbits-dark">
<q-header bordered class="bg-marginal-bg">
<q-toolbar>
{% block drawer_toggle %}
<q-btn
@ -46,13 +47,82 @@
>
</q-badge>
{% endblock %}
<q-btn-dropdown
v-if="g.allowedThemes"
dense
flat
round
size="sm"
icon="dashboard_customize"
class="q-pl-md"
>
<div class="row no-wrap q-pa-md">
<q-btn
dense
flat
@click="changeColor('classic')"
icon="format_color_fill"
color="deep-purple"
size="md"
><q-tooltip>classic</q-tooltip> </q-btn
><q-btn
v-if="g.allowedThemes.includes('mint')"
dense
flat
@click="changeColor('mint')"
icon="format_color_fill"
color="green"
size="md"
><q-tooltip>mint</q-tooltip> </q-btn
><q-btn
v-if="g.allowedThemes.includes('autumn')"
dense
flat
@click="changeColor('autumn')"
icon="format_color_fill"
color="brown"
size="md"
><q-tooltip>autumn</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('flamingo')"
dense
flat
@click="changeColor('monochrome')"
icon="format_color_fill"
color="grey"
size="md"
><q-tooltip>monochrome</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('monochrome')"
dense
flat
@click="changeColor('salvador')"
icon="format_color_fill"
color="blue-10"
size="md"
><q-tooltip>elSalvador</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('quasar')"
dense
flat
@click="changeColor('flamingo')"
icon="format_color_fill"
color="pink-3"
size="md"
><q-tooltip>flamingo</q-tooltip>
</q-btn>
</div>
</q-btn-dropdown>
<q-btn
dense
flat
round
@click="toggleDarkMode"
:icon="($q.dark.isActive) ? 'brightness_3' : 'wb_sunny'"
class="q-ml-lg"
size="sm"
>
<q-tooltip>Toggle Dark Mode</q-tooltip>
@ -93,7 +163,7 @@
<q-btn
flat
dense
:color="($q.dark.isActive) ? 'white' : 'deep-purple'"
:color="($q.dark.isActive) ? 'white' : 'primary'"
icon="code"
type="a"
href="https://github.com/lnbits/lnbits"
@ -115,6 +185,12 @@
<!---->
<script src="/static/js/base.js"></script>
<script src="/static/js/components.js"></script>
<script type="text/javascript">
const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
if(themes && themes.length) {
window.allowedThemes = themes.trim()
}
</script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -1,4 +1,4 @@
import trio # type: ignore
import trio
import httpx
from typing import Callable, NamedTuple

View File

@ -3,7 +3,7 @@ try:
except ImportError: # pragma: nocover
LightningRpc = None
import trio # type: ignore
import trio
import random
import json

View File

@ -1,4 +1,4 @@
import trio # type: ignore
import trio
import json
import httpx
from os import getenv

View File

@ -1,4 +1,4 @@
import trio # type: ignore
import trio
import httpx
import json
import base64

View File

@ -1,5 +1,5 @@
import json
import trio # type: ignore
import trio
import httpx
from os import getenv
from http import HTTPStatus

View File

@ -1,4 +1,4 @@
import trio # type: ignore
import trio
import json
import httpx
from os import getenv

View File

@ -1,4 +1,4 @@
import trio # type: ignore
import trio
import hmac
import httpx
from http import HTTPStatus

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