Merge branch 'master' into jukebox/addlistener
@ -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
|
||||
|
2
Pipfile
@ -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
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -1,4 +1,4 @@
|
||||
import trio # type: ignore
|
||||
import trio
|
||||
|
||||
from .commands import migrate_databases, transpile_scss, bundle_vendored
|
||||
|
||||
|
@ -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():
|
||||
|
@ -1,4 +1,4 @@
|
||||
import trio # type: ignore
|
||||
import trio
|
||||
import warnings
|
||||
import click
|
||||
import importlib
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
import trio # type: ignore
|
||||
import trio
|
||||
import httpx
|
||||
from typing import List
|
||||
|
||||
|
@ -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('')"
|
||||
>
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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()))
|
||||
|
@ -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
|
||||
|
2
lnbits/data/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 ||
|
||||
|
@ -24,7 +24,7 @@
|
||||
dense
|
||||
flat
|
||||
icon="check"
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
type="submit"
|
||||
@click="createInvoice"
|
||||
:disabled="userAmount < captchaAmount || paymentReq"
|
||||
|
@ -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
|
||||
|
3
lnbits/extensions/copilot/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# StreamerCopilot
|
||||
|
||||
Tool to help streamers accept sats for tips
|
17
lnbits/extensions/copilot/__init__.py
Normal 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))
|
8
lnbits/extensions/copilot/config.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "StreamerCopilot",
|
||||
"short_description": "Video tips/animations/webhooks",
|
||||
"icon": "face",
|
||||
"contributors": [
|
||||
"arcbtc"
|
||||
]
|
||||
}
|
109
lnbits/extensions/copilot/crud.py
Normal 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,))
|
86
lnbits/extensions/copilot/lnurl.py
Normal 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())
|
33
lnbits/extensions/copilot/migrations.py
Normal 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'))
|
||||
);
|
||||
"""
|
||||
)
|
41
lnbits/extensions/copilot/models.py
Normal 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)
|
BIN
lnbits/extensions/copilot/static/bitcoin.gif
Normal file
After Width: | Height: | Size: 308 KiB |
BIN
lnbits/extensions/copilot/static/confetti.gif
Normal file
After Width: | Height: | Size: 333 KiB |
BIN
lnbits/extensions/copilot/static/face.gif
Normal file
After Width: | Height: | Size: 536 KiB |
BIN
lnbits/extensions/copilot/static/lnurl.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
lnbits/extensions/copilot/static/martijn.gif
Normal file
After Width: | Height: | Size: 504 KiB |
BIN
lnbits/extensions/copilot/static/rick.gif
Normal file
After Width: | Height: | Size: 2.3 MiB |
BIN
lnbits/extensions/copilot/static/rocket.gif
Normal file
After Width: | Height: | Size: 577 KiB |
88
lnbits/extensions/copilot/tasks.py
Normal 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),
|
||||
)
|
172
lnbits/extensions/copilot/templates/copilot/_api_docs.html
Normal 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": <admin_key>}</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>[<copilot_object>, ...]</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":
|
||||
<string>, "animation": <string>,
|
||||
"show_message":<string>, "amount": <integer>,
|
||||
"lnurl_title": <string>}' -H "Content-type: application/json"
|
||||
-H "X-Api-Key: {{g.user.wallets[0].adminkey }}"
|
||||
</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/<copilot_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</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>[<copilot_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root
|
||||
}}api/v1/copilot/<copilot_id> -d '{"title": <string>,
|
||||
"animation": <string>, "show_message":<string>,
|
||||
"amount": <integer>, "lnurl_title": <string>}' -H
|
||||
"Content-type: application/json" -H "X-Api-Key:
|
||||
{{g.user.wallets[0].adminkey }}"
|
||||
</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/<copilot_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</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>[<copilot_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/copilot/<copilot_id>
|
||||
-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": <invoice_key>}</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>[<copilot_object>, ...]</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/<copilot_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</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/<copilot_id> -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/<copilot_id>/<comment>/<data></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</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/<string,
|
||||
copilot_id>/<string, comment>/<string, gif name> -H
|
||||
"X-Api-Key: {{ g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
||||
</q-card>
|
289
lnbits/extensions/copilot/templates/copilot/compose.html
Normal 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 %}
|
637
lnbits/extensions/copilot/templates/copilot/index.html
Normal 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 %}
|
157
lnbits/extensions/copilot/templates/copilot/panel.html
Normal 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 %}
|
61
lnbits/extensions/copilot/views.py
Normal 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}")
|
109
lnbits/extensions/copilot/views_api.py
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
@ -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"\
|
||||

|
||||
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\
|
||||

|
||||
- follow the steps to get your Spotify App and get the client ID and secret key\
|
||||

|
||||
- paste the codes in the form\
|
||||

|
||||
- copy the _Redirect URL_ presented on the form\
|
||||

|
||||
- on Spotify click the "EDIT SETTINGS" button and paste the copied link in the _Redirect URI's_ prompt
|
||||

|
||||
- 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)\
|
||||

|
||||
|
||||
3. After Jukebox is created, click the icon to open the dialog with the shareable QR, open the Jukebox page, etc...\
|
||||

|
||||
4. The users will see the Jukebox page and choose a song from the selected playlist\
|
||||

|
||||
5. After selecting a song they'd like to hear next a dialog will show presenting the music\
|
||||

|
||||
6. After payment, the song will automatically start playing on the device selected or enter the queue if some other music is already playing
|
||||
|
@ -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(
|
||||
|
@ -6,14 +6,9 @@ new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
|
||||
},
|
||||
created() {
|
||||
|
||||
}
|
||||
methods: {},
|
||||
created() {}
|
||||
})
|
||||
|
@ -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": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
@ -27,7 +36,8 @@
|
||||
</h5>
|
||||
<code>[<jukebox_object>, ...]</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/<juke_id></code>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/jukebox/api/v1/jukebox/<juke_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
@ -46,36 +58,44 @@
|
||||
</h5>
|
||||
<code><jukebox_object></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X GET {{ request.url_root }}api/v1/jukebox/<juke_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].adminkey }}"
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/jukebox/<juke_id> -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": <admin_key>}</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><jukbox_object></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": <string, user_id>,
|
||||
"title": <string>, "wallet":<string>, "sp_user":
|
||||
<string, spotify_user_account>, "sp_secret": <string, spotify_user_secret>, "sp_access_token":
|
||||
<string, not_required>, "sp_refresh_token":
|
||||
<string, not_required>, "sp_device": <string, spotify_user_secret>, "sp_playlists":
|
||||
<string, not_required>, "price":
|
||||
<integer, not_required>}' -H "Content-type:
|
||||
application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}"
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}api/v1/jukebox/ -d '{"user":
|
||||
<string, user_id>, "title": <string>,
|
||||
"wallet":<string>, "sp_user": <string,
|
||||
spotify_user_account>, "sp_secret": <string,
|
||||
spotify_user_secret>, "sp_access_token": <string,
|
||||
not_required>, "sp_refresh_token": <string, not_required>,
|
||||
"sp_device": <string, spotify_user_secret>, "sp_playlists":
|
||||
<string, not_required>, "price": <integer, not_required>}'
|
||||
-H "Content-type: application/json" -H "X-Api-Key:
|
||||
{{g.user.wallets[0].adminkey }}"
|
||||
</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/<juke_id></code>
|
||||
<code
|
||||
><span class="text-red">DELETE</span>
|
||||
/jukebox/api/v1/jukebox/<juke_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
@ -93,9 +115,11 @@
|
||||
</h5>
|
||||
<code><jukebox_object></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X DELETE {{ request.url_root }}api/v1/jukebox/<juke_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].adminkey }}"
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root }}api/v1/jukebox/<juke_id>
|
||||
-H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item></q-expansion-item
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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'
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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))
|
||||
|
37
lnbits/extensions/lnticket/tasks.py
Normal 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)
|
@ -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>
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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 => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import trio # type: ignore
|
||||
import trio
|
||||
import json
|
||||
import httpx
|
||||
|
||||
|
@ -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": <invoice_key>}</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/<pay_id></code
|
||||
><span class="text-blue">GET</span> /lnurlp/api/v1/links/<pay_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</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": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code>{"description": <string> "amount": <integer>}</code>
|
||||
<code>{"description": <string> "amount": <integer> "max": <integer> "min": <integer> "comment_chars": <integer>}</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":
|
||||
<string>, "amount": <integer>}' -H "Content-type:
|
||||
<string>, "amount": <integer>, "max": <integer>, "min": <integer>, "comment_chars": <integer>}' -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/<pay_id></code
|
||||
/lnurlp/api/v1/links/<pay_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
@ -111,7 +111,7 @@
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-pink">DELETE</span>
|
||||
/api/v1/links/<pay_id></code
|
||||
/lnurlp/api/v1/links/<pay_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
|
@ -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 ||
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -24,7 +24,7 @@
|
||||
dense
|
||||
flat
|
||||
icon="check"
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
type="submit"
|
||||
@click="createInvoice"
|
||||
:disabled="userAmount < paywallAmount || paymentReq"
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||

|
||||
|
||||
2. Add the wallet or wallets info to split payments to
|
||||
|
||||
 - 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\
|
||||
 - 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\
|
||||

|
||||
- source wallet gets 18 sats\
|
||||

|
||||
- Ben's wallet (the wallet from the example) instantly, and feeless, gets the corresponding 10%, or 2 sats\
|
||||

|
||||
|
||||
## Sponsored by
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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 }}'"
|
||||
|
@ -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>
|
||||
|
@ -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")
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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>:
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import trio # type: ignore
|
||||
import trio
|
||||
import httpx
|
||||
from typing import Callable, NamedTuple
|
||||
|
||||
|
@ -3,7 +3,7 @@ try:
|
||||
except ImportError: # pragma: nocover
|
||||
LightningRpc = None
|
||||
|
||||
import trio # type: ignore
|
||||
import trio
|
||||
import random
|
||||
import json
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import trio # type: ignore
|
||||
import trio
|
||||
import json
|
||||
import httpx
|
||||
from os import getenv
|
||||
|
@ -1,4 +1,4 @@
|
||||
import trio # type: ignore
|
||||
import trio
|
||||
import httpx
|
||||
import json
|
||||
import base64
|
||||
|
@ -1,5 +1,5 @@
|
||||
import json
|
||||
import trio # type: ignore
|
||||
import trio
|
||||
import httpx
|
||||
from os import getenv
|
||||
from http import HTTPStatus
|
||||
|
@ -1,4 +1,4 @@
|
||||
import trio # type: ignore
|
||||
import trio
|
||||
import json
|
||||
import httpx
|
||||
from os import getenv
|
||||
|
@ -1,4 +1,4 @@
|
||||
import trio # type: ignore
|
||||
import trio
|
||||
import hmac
|
||||
import httpx
|
||||
from http import HTTPStatus
|
||||
|