diff --git a/.env.example b/.env.example index cc70644c..6ca9a281 100644 --- a/.env.example +++ b/.env.example @@ -5,14 +5,29 @@ QUART_DEBUG=true HOST=127.0.0.1 PORT=5000 -LNBITS_SITE_TITLE=LNbits + LNBITS_ALLOWED_USERS="" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" + +# Database: to use SQLite, specify LNBITS_DATA_FOLDER +# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://... +# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://... +# for both PostgreSQL and CockroachDB, you'll need to install +# psycopg2 as an additional dependency LNBITS_DATA_FOLDER="./data" -LNBITS_DISABLED_EXTENSIONS="amilk" +# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename" + +LNBITS_DISABLED_EXTENSIONS="amilk,ngrok" LNBITS_FORCE_HTTPS=true LNBITS_SERVICE_FEE="0.0" +# Change theme +LNBITS_SITE_TITLE="LNbits" +LNBITS_SITE_TAGLINE="free and open-source lightning wallet" +LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'" +# Choose from mint, flamingo, salvador, autumn, monochrome, classic +LNBITS_THEME_OPTIONS="mint, flamingo, classic, autumn, monochrome, salvador" + # Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC), # LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet LNBITS_BACKEND_WALLET_CLASS=VoidWallet diff --git a/.gitignore b/.gitignore index ca3fcd00..79e10fb8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__ *$py.class .mypy_cache .vscode +*-lock.json *.egg *.egg-info diff --git a/Pipfile b/Pipfile index ae59e0b1..951ea9bf 100644 --- a/Pipfile +++ b/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 = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 0dc59dc5..1b1212d7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e12af74353e8bea3f97bf2aea16a1ba0a6e4c3a08042ce7368187a06e7791e2c" + "sha256": "4067e94f45066ab088fc12ce09371b360c2bdb6b29f10c84f8ca06b3a9ede22a" }, "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,51 @@ }, "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" }, "outcome": { "hashes": [ @@ -316,31 +306,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 +356,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 +379,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 +406,10 @@ "idna2008" ], "hashes": [ - "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d", - "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50" + "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", + "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" ], - "version": "==1.4.0" - }, - "secure": { - "hashes": [ - "sha256:4dc8dd4b548831c3ad7f94079332c41d67c781eccc32215ff5a8a49582c1a447", - "sha256:b3bf1e39ebf40040fc3248392343a5052aa14cb45fc87ec91b0bd11f19cc46bd" - ], - "index": "pypi", - "version": "==0.2.1" + "version": "==1.5.0" }, "shortuuid": { "hashes": [ @@ -439,11 +421,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 +437,10 @@ }, "sortedcontainers": { "hashes": [ - "sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f", - "sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1" + "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", + "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" ], - "version": "==2.3.0" + "version": "==2.4.0" }, "sqlalchemy": { "hashes": [ @@ -530,20 +512,20 @@ }, "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" }, "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 +554,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 +569,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 +635,10 @@ }, "idna": { "hashes": [ - "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", - "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" + "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", + "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" ], - "version": "==3.1" + "version": "==3.2" }, "iniconfig": { "hashes": [ @@ -667,31 +649,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" + "version": "==0.902" }, "mypy-extensions": { "hashes": [ @@ -749,19 +732,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 +809,10 @@ }, "sortedcontainers": { "hashes": [ - "sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f", - "sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1" + "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", + "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" ], - "version": "==2.3.0" + "version": "==2.4.0" }, "toml": { "hashes": [ @@ -847,6 +830,14 @@ "index": "pypi", "version": "==0.16.0" }, + "trio-typing": { + "hashes": [ + "sha256:35f1bec8df2150feab6c8b073b54135321722c9d9289bbffa78a9a091ea83b72", + "sha256:f2007df617a6c26a2294db0dd63645b5451149757e1bde4cb8dbf3e1369174fb" + ], + "index": "pypi", + "version": "==0.5.0" + }, "typed-ast": { "hashes": [ "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace", @@ -884,12 +875,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" } } } diff --git a/docs/devs/installation.md b/docs/devs/installation.md index 55636181..013f7be9 100644 --- a/docs/devs/installation.md +++ b/docs/devs/installation.md @@ -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 diff --git a/docs/guide/installation.md b/docs/guide/installation.md index de309086..9fb8a3e3 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -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 +cd lnbits/ # ${PWD} referred as docker build -t lnbits . ``` @@ -57,4 +57,4 @@ 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. diff --git a/lnbits/__main__.py b/lnbits/__main__.py index fa75231c..90b08642 100644 --- a/lnbits/__main__.py +++ b/lnbits/__main__.py @@ -1,4 +1,4 @@ -import trio # type: ignore +import trio from .commands import migrate_databases, transpile_scss, bundle_vendored diff --git a/lnbits/app.py b/lnbits/app.py index 35852cd9..f264bf4a 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -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,13 @@ 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["SITE_TAGLINE"] = app.config["LNBITS_SITE_TAGLINE"] + app.jinja_env.globals["SITE_DESCRIPTION"] = app.config["LNBITS_SITE_DESCRIPTION"] + 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(): diff --git a/lnbits/bolt11.py b/lnbits/bolt11.py index 6acc6db7..b9f3270b 100644 --- a/lnbits/bolt11.py +++ b/lnbits/bolt11.py @@ -161,9 +161,9 @@ def _trim_to_bytes(barr): def _readable_scid(short_channel_id: int) -> str: return "{blockheight}x{transactionindex}x{outputindex}".format( - blockheight=((short_channel_id >> 40) & 0xFFFFFF), - transactionindex=((short_channel_id >> 16) & 0xFFFFFF), - outputindex=(short_channel_id & 0xFFFF), + blockheight=((short_channel_id >> 40) & 0xffffff), + transactionindex=((short_channel_id >> 16) & 0xffffff), + outputindex=(short_channel_id & 0xffff), ) diff --git a/lnbits/commands.py b/lnbits/commands.py index 2be04d12..021d26dc 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -1,11 +1,11 @@ -import trio # type: ignore +import trio import warnings import click import importlib import re import os -from sqlalchemy.exc import OperationalError # type: ignore +from .db import SQLITE, POSTGRES, COCKROACH from .core import db as core_db, migrations as core_migrations from .helpers import ( get_valid_extensions, @@ -53,41 +53,59 @@ def bundle_vendored(): async def migrate_databases(): """Creates the necessary databases if they don't exist already; or migrates them.""" - async with core_db.connect() as conn: - try: - rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall() - except OperationalError: - # migration 3 wasn't ran - await core_migrations.m000_create_migrations_table(conn) - rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall() + async def set_migration_version(conn, db_name, version): + await conn.execute( + """ + INSERT INTO dbversions (db, version) VALUES (?, ?) + ON CONFLICT (db) DO UPDATE SET version = ? + """, + (db_name, version, version), + ) + async def run_migration(db, migrations_module): + db_name = migrations_module.__name__.split(".")[-2] + for key, migrate in migrations_module.__dict__.items(): + match = match = matcher.match(key) + if match: + version = int(match.group(1)) + if version > current_versions.get(db_name, 0): + print(f"running migration {db_name}.{version}") + await migrate(db) + + if db.schema == None: + await set_migration_version(db, db_name, version) + else: + async with core_db.connect() as conn: + await set_migration_version(conn, db_name, version) + + async with core_db.connect() as conn: + if conn.type == SQLITE: + exists = await conn.fetchone( + "SELECT * FROM sqlite_master WHERE type='table' AND name='dbversions'" + ) + elif conn.type in {POSTGRES, COCKROACH}: + exists = await conn.fetchone( + "SELECT * FROM information_schema.tables WHERE table_name = 'dbversions'" + ) + + if not exists: + await core_migrations.m000_create_migrations_table(conn) + + rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall() current_versions = {row["db"]: row["version"] for row in rows} matcher = re.compile(r"^m(\d\d\d)_") - - async def run_migration(db, migrations_module): - db_name = migrations_module.__name__.split(".")[-2] - for key, migrate in migrations_module.__dict__.items(): - match = match = matcher.match(key) - if match: - version = int(match.group(1)) - if version > current_versions.get(db_name, 0): - print(f"running migration {db_name}.{version}") - await migrate(db) - await conn.execute( - "INSERT OR REPLACE INTO dbversions (db, version) VALUES (?, ?)", - (db_name, version), - ) - await run_migration(conn, core_migrations) - for ext in get_valid_extensions(): - try: - ext_migrations = importlib.import_module( - f"lnbits.extensions.{ext.code}.migrations" - ) - ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db - await run_migration(ext_db, ext_migrations) - except ImportError: - raise ImportError( - f"Please make sure that the extension `{ext.code}` has a migrations file." - ) + for ext in get_valid_extensions(): + try: + ext_migrations = importlib.import_module( + f"lnbits.extensions.{ext.code}.migrations" + ) + ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db + except ImportError: + raise ImportError( + f"Please make sure that the extension `{ext.code}` has a migrations file." + ) + + async with ext_db.connect() as ext_conn: + await run_migration(ext_conn, ext_migrations) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 47623cc2..8135dc88 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -5,7 +5,7 @@ from typing import List, Optional, Dict, Any from urllib.parse import urlparse from lnbits import bolt11 -from lnbits.db import Connection +from lnbits.db import Connection, POSTGRES, COCKROACH from lnbits.settings import DEFAULT_WALLET_NAME from . import db @@ -43,13 +43,14 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[ if user: extensions = await (conn or db).fetchall( - "SELECT extension FROM extensions WHERE user = ? AND active = 1", (user_id,) + """SELECT extension FROM extensions WHERE "user" = ? AND active""", + (user_id,), ) wallets = await (conn or db).fetchall( """ SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat FROM wallets - WHERE user = ? + WHERE "user" = ? """, (user_id,), ) @@ -70,14 +71,14 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[ async def update_user_extension( - *, user_id: str, extension: str, active: int, conn: Optional[Connection] = None + *, user_id: str, extension: str, active: bool, conn: Optional[Connection] = None ) -> None: await (conn or db).execute( """ - INSERT OR REPLACE INTO extensions (user, extension, active) - VALUES (?, ?, ?) + INSERT INTO extensions ("user", extension, active) VALUES (?, ?, ?) + ON CONFLICT ("user", extension) DO UPDATE SET active = ? """, - (user_id, extension, active), + (user_id, extension, active, active), ) @@ -94,7 +95,7 @@ async def create_wallet( wallet_id = uuid4().hex await (conn or db).execute( """ - INSERT INTO wallets (id, name, user, adminkey, inkey) + INSERT INTO wallets (id, name, "user", adminkey, inkey) VALUES (?, ?, ?, ?, ?) """, ( @@ -119,10 +120,10 @@ async def delete_wallet( """ UPDATE wallets AS w SET - user = 'del:' || w.user, + "user" = 'del:' || w."user", adminkey = 'del:' || w.adminkey, inkey = 'del:' || w.inkey - WHERE id = ? AND user = ? + WHERE id = ? AND "user" = ? """, (wallet_id, user_id), ) @@ -218,7 +219,12 @@ async def get_payments( clause: List[str] = [] if since != None: - clause.append("time > ?") + if db.type == POSTGRES: + clause.append("time > to_timestamp(?)") + elif db.type == COCKROACH: + clause.append("time > cast(? AS timestamp)") + else: + clause.append("time > ?") args.append(since) if wallet_id: @@ -228,9 +234,9 @@ async def get_payments( if complete and pending: pass elif complete: - clause.append("((amount > 0 AND pending = 0) OR amount < 0)") + clause.append("((amount > 0 AND pending = false) OR amount < 0)") elif pending: - clause.append("pending = 1") + clause.append("pending = true") else: pass @@ -269,20 +275,21 @@ async def delete_expired_invoices( ) -> None: # first we delete all invoices older than one month await (conn or db).execute( - """ + f""" DELETE FROM apipayments - WHERE pending = 1 AND amount > 0 AND time < strftime('%s', 'now') - 2592000 + WHERE pending = true AND amount > 0 + AND time < {db.timestamp_now} - {db.interval_seconds(2592000)} """ ) # then we delete all expired invoices, checking one by one rows = await (conn or db).fetchall( - """ + f""" SELECT bolt11 FROM apipayments - WHERE pending = 1 + WHERE pending = true AND bolt11 IS NOT NULL - AND amount > 0 AND time < strftime('%s', 'now') - 86400 + AND amount > 0 AND time < {db.timestamp_now} - {db.interval_seconds(86400)} """ ) for (payment_request,) in rows: @@ -298,7 +305,7 @@ async def delete_expired_invoices( await (conn or db).execute( """ DELETE FROM apipayments - WHERE pending = 1 AND hash = ? + WHERE pending = true AND hash = ? """, (invoice.payment_hash,), ) @@ -337,7 +344,7 @@ async def create_payment( payment_hash, preimage, amount, - int(pending), + pending, memo, fee, json.dumps(extra) @@ -361,7 +368,7 @@ async def update_payment_status( await (conn or db).execute( "UPDATE apipayments SET pending = ? WHERE checking_id = ?", ( - int(pending), + pending, checking_id, ), ) @@ -406,10 +413,10 @@ async def save_balance_check( await (conn or db).execute( """ - INSERT OR REPLACE INTO balance_check (wallet, service, url) - VALUES (?, ?, ?) + INSERT INTO balance_check (wallet, service, url) VALUES (?, ?, ?) + ON CONFLICT (wallet, service) DO UPDATE SET url = ? """, - (wallet_id, domain, url), + (wallet_id, domain, url, url), ) @@ -445,10 +452,10 @@ async def save_balance_notify( ): await (conn or db).execute( """ - INSERT OR REPLACE INTO balance_notify (wallet, url) - VALUES (?, ?) + INSERT INTO balance_notify (wallet, url) VALUES (?, ?) + ON CONFLICT (wallet) DO UPDATE SET url = ? """, - (wallet_id, url), + (wallet_id, url, url), ) diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 64de9acf..46686716 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -18,7 +18,7 @@ async def m001_initial(db): """ await db.execute( """ - CREATE TABLE IF NOT EXISTS accounts ( + CREATE TABLE accounts ( id TEXT PRIMARY KEY, email TEXT, pass TEXT @@ -27,37 +27,36 @@ async def m001_initial(db): ) await db.execute( """ - CREATE TABLE IF NOT EXISTS extensions ( - user TEXT NOT NULL, + CREATE TABLE extensions ( + "user" TEXT NOT NULL, extension TEXT NOT NULL, - active BOOLEAN DEFAULT 0, + active BOOLEAN DEFAULT false, - UNIQUE (user, extension) + UNIQUE ("user", extension) ); """ ) await db.execute( """ - CREATE TABLE IF NOT EXISTS wallets ( + CREATE TABLE wallets ( id TEXT PRIMARY KEY, name TEXT NOT NULL, - user TEXT NOT NULL, + "user" TEXT NOT NULL, adminkey TEXT NOT NULL, inkey TEXT ); """ ) await db.execute( - """ - CREATE TABLE IF NOT EXISTS apipayments ( + f""" + CREATE TABLE apipayments ( payhash TEXT NOT NULL, amount INTEGER NOT NULL, fee INTEGER NOT NULL DEFAULT 0, wallet TEXT NOT NULL, pending BOOLEAN NOT NULL, memo TEXT, - time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')), - + time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, UNIQUE (wallet, payhash) ); """ @@ -65,18 +64,18 @@ async def m001_initial(db): await db.execute( """ - CREATE VIEW IF NOT EXISTS balances AS + CREATE VIEW balances AS SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM ( SELECT wallet, SUM(amount) AS s -- incoming FROM apipayments - WHERE amount > 0 AND pending = 0 -- don't sum pending + WHERE amount > 0 AND pending = false -- don't sum pending GROUP BY wallet UNION ALL SELECT wallet, SUM(amount + fee) AS s -- outgoing, sum fees FROM apipayments WHERE amount < 0 -- do sum pending GROUP BY wallet - ) + )x GROUP BY wallet; """ ) @@ -143,21 +142,20 @@ async def m004_ensure_fees_are_always_negative(db): """ await db.execute("DROP VIEW balances") - await db.execute( """ - CREATE VIEW IF NOT EXISTS balances AS + CREATE VIEW balances AS SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM ( SELECT wallet, SUM(amount) AS s -- incoming FROM apipayments - WHERE amount > 0 AND pending = 0 -- don't sum pending + WHERE amount > 0 AND pending = false -- don't sum pending GROUP BY wallet UNION ALL SELECT wallet, SUM(amount - abs(fee)) AS s -- outgoing, sum fees FROM apipayments WHERE amount < 0 -- do sum pending GROUP BY wallet - ) + )x GROUP BY wallet; """ ) @@ -171,7 +169,7 @@ async def m005_balance_check_balance_notify(db): await db.execute( """ CREATE TABLE balance_check ( - wallet INTEGER NOT NULL REFERENCES wallets (id), + wallet TEXT NOT NULL REFERENCES wallets (id), service TEXT NOT NULL, url TEXT NOT NULL, @@ -183,7 +181,7 @@ async def m005_balance_check_balance_notify(db): await db.execute( """ CREATE TABLE balance_notify ( - wallet INTEGER NOT NULL REFERENCES wallets (id), + wallet TEXT NOT NULL REFERENCES wallets (id), url TEXT NOT NULL, UNIQUE(wallet, url) diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 13e56c53..09b9f4f7 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -1,4 +1,4 @@ -import trio # type: ignore +import trio import json import httpx from io import BytesIO diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index d0191051..71282408 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -202,9 +202,7 @@ new Vue({ return this.parse.invoice.sat <= this.balance }, pendingPaymentsExist: function () { - return this.payments - ? _.where(this.payments, {pending: 1}).length > 0 - : false + return this.payments.findIndex(payment => payment.pending) !== -1 } }, filters: { diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py index 3a296e66..fa2df964 100644 --- a/lnbits/core/tasks.py +++ b/lnbits/core/tasks.py @@ -1,4 +1,4 @@ -import trio # type: ignore +import trio import httpx from typing import List diff --git a/lnbits/core/templates/core/extensions.html b/lnbits/core/templates/core/extensions.html index 97fa8936..daeb660f 100644 --- a/lnbits/core/templates/core/extensions.html +++ b/lnbits/core/templates/core/extensions.html @@ -17,14 +17,14 @@ > {% raw %}
{{ extension.name }}
- {{ extension.shortDescription }} {% endraw %} + {{ extension.shortDescription }} {% endraw %}
Open diff --git a/lnbits/core/templates/core/index.html b/lnbits/core/templates/core/index.html index 7ca61a3a..a7e9c39d 100644 --- a/lnbits/core/templates/core/index.html +++ b/lnbits/core/templates/core/index.html @@ -8,7 +8,7 @@ {% if lnurl %} Add a new wallet -

LNbits

-
Free and open-source lightning wallet
-

- Easy to set up and lightweight, LNbits can run on any - lightning-network funding source, currently supporting LND, - c-lightning, OpenNode, lntxbot, LNPay and even LNbits itself! -

-

- You can run LNbits for yourself, or easily offer a custodian solution - for others. -

-

- Each wallet has its own API keys and there is no limit to the number - of wallets you can make. Being able to partition funds makes LNbits a - useful tool for money management and as a development tool. -

-

- Extensions add extra functionality to LNbits so you can experiment - with a range of cutting-edge technologies on the lightning network. We - have made developing extensions as easy as possible, and as a free and - open-source project, we encourage people to develop and submit their - own. -

-
- View project in GitHub - Donate +

{{SITE_TITLE}}

+
{{SITE_TAGLINE}}
+
+

+ Easy to set up and lightweight, LNbits can run on any + lightning-network funding source, currently supporting LND, + c-lightning, OpenNode, lntxbot, LNPay and even LNbits itself! +

+

+ You can run LNbits for yourself, or easily offer a custodian + solution for others. +

+

+ Each wallet has its own API keys and there is no limit to the number + of wallets you can make. Being able to partition funds makes LNbits + a useful tool for money management and as a development tool. +

+

+ Extensions add extra functionality to LNbits so you can experiment + with a range of cutting-edge technologies on the lightning network. + We have made developing extensions as easy as possible, and as a + free and open-source project, we encourage people to develop and + submit their own. +

+
+ View project in GitHub + Donate +
+

{{SITE_DESCRIPTION}}

-
+
- +
diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index d127b6f9..31eba13d 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -22,7 +22,7 @@
Paste Request Create Invoice scan @@ -222,7 +222,7 @@
- LNbits wallet: {{ wallet.name }} + {{ SITE_TITLE }} wallet: {{ wallet.name }}
@@ -342,7 +342,7 @@
@@ -355,7 +355,7 @@
@@ -395,7 +395,7 @@

{% endraw %}
- Pay + Pay Cancel
@@ -423,7 +423,7 @@ {{ parse.lnurlauth.pubkey }}

- Login + Login Cancel @@ -485,9 +485,7 @@
- Send satoshis + Send satoshis Cancel @@ -512,7 +510,7 @@
Read str: + if self.type in {POSTGRES, COCKROACH}: + return f"interval '{seconds} seconds'" + elif self.type == SQLITE: + return f"{seconds}" + return "" + + @property + def timestamp_now(self) -> str: + if self.type in {POSTGRES, COCKROACH}: + return "now()" + elif self.type == SQLITE: + return "(strftime('%s', 'now'))" + return "" + + @property + def serial_primary_key(self) -> str: + if self.type in {POSTGRES, COCKROACH}: + return "SERIAL PRIMARY KEY" + elif self.type == SQLITE: + return "INTEGER PRIMARY KEY AUTOINCREMENT" + return "" + + @property + def references_schema(self) -> str: + if self.type in {POSTGRES, COCKROACH}: + return f"{self.schema}." + elif self.type == SQLITE: + return "" + return "" + + +class Connection(Compat): + def __init__(self, conn: AsyncConnection, txn, typ, name, schema): self.conn = conn + self.txn = txn + self.type = typ + self.name = name + self.schema = schema + + def rewrite_query(self, query) -> str: + if self.type in {POSTGRES, COCKROACH}: + query = query.replace("%", "%%") + query = query.replace("?", "%s") + return query async def fetchall(self, query: str, values: tuple = ()) -> list: - result = await self.conn.execute(query, values) + result = await self.conn.execute(self.rewrite_query(query), values) return await result.fetchall() async def fetchone(self, query: str, values: tuple = ()): - result = await self.conn.execute(query, values) + result = await self.conn.execute(self.rewrite_query(query), values) row = await result.fetchone() await result.close() return row async def execute(self, query: str, values: tuple = ()): - return await self.conn.execute(query, values) + return await self.conn.execute(self.rewrite_query(query), values) -class Database: +class Database(Compat): def __init__(self, db_name: str): - self.db_name = db_name - db_path = os.path.join(LNBITS_DATA_FOLDER, f"{db_name}.sqlite3") - self.engine = create_engine(f"sqlite:///{db_path}", strategy=TRIO_STRATEGY) + self.name = db_name + + if LNBITS_DATABASE_URL: + database_uri = LNBITS_DATABASE_URL + + if database_uri.startswith("cockroachdb://"): + self.type = COCKROACH + else: + self.type = POSTGRES + + import psycopg2 # type: ignore + + psycopg2.extensions.register_type( + psycopg2.extensions.new_type( + psycopg2.extensions.DECIMAL.values, + "DEC2FLOAT", + lambda value, curs: float(value) if value is not None else None, + ) + ) + psycopg2.extensions.register_type( + psycopg2.extensions.new_type( + psycopg2.extensions.TIME.values + psycopg2.extensions.DATE.values, + "DATE2INT", + lambda value, curs: time.mktime(value.timetuple()) + if value is not None + else None, + ) + ) + else: + self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3") + database_uri = f"sqlite:///{self.path}" + self.type = SQLITE + + self.schema = self.name + if self.name.startswith("ext_"): + self.schema = self.name[4:] + else: + self.schema = None + + self.engine = create_engine(database_uri, strategy=TRIO_STRATEGY) self.lock = trio.StrictFIFOLock() @asynccontextmanager @@ -38,8 +127,20 @@ class Database: await self.lock.acquire() try: async with self.engine.connect() as conn: - async with conn.begin(): - yield Connection(conn) + async with conn.begin() as txn: + wconn = Connection(conn, txn, self.type, self.name, self.schema) + + if self.schema: + if self.type in {POSTGRES, COCKROACH}: + await wconn.execute( + f"CREATE SCHEMA IF NOT EXISTS {self.schema}" + ) + elif self.type == SQLITE: + await wconn.execute( + f"ATTACH '{self.path}' AS {self.schema}" + ) + + yield wconn finally: self.lock.release() diff --git a/lnbits/extensions/amilk/crud.py b/lnbits/extensions/amilk/crud.py index 773caa42..859d2fa8 100644 --- a/lnbits/extensions/amilk/crud.py +++ b/lnbits/extensions/amilk/crud.py @@ -10,7 +10,7 @@ async def create_amilk(*, wallet_id: str, lnurl: str, atime: int, amount: int) - amilk_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8") await db.execute( """ - INSERT INTO amilks (id, wallet, lnurl, atime, amount) + INSERT INTO amilk.amilks (id, wallet, lnurl, atime, amount) VALUES (?, ?, ?, ?, ?) """, (amilk_id, wallet_id, lnurl, atime, amount), @@ -22,7 +22,7 @@ async def create_amilk(*, wallet_id: str, lnurl: str, atime: int, amount: int) - async def get_amilk(amilk_id: str) -> Optional[AMilk]: - row = await db.fetchone("SELECT * FROM amilks WHERE id = ?", (amilk_id,)) + row = await db.fetchone("SELECT * FROM amilk.amilks WHERE id = ?", (amilk_id,)) return AMilk(**row) if row else None @@ -32,11 +32,11 @@ async def get_amilks(wallet_ids: Union[str, List[str]]) -> List[AMilk]: q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall( - f"SELECT * FROM amilks WHERE wallet IN ({q})", (*wallet_ids,) + f"SELECT * FROM amilk.amilks WHERE wallet IN ({q})", (*wallet_ids,) ) return [AMilk(**row) for row in rows] async def delete_amilk(amilk_id: str) -> None: - await db.execute("DELETE FROM amilks WHERE id = ?", (amilk_id,)) + await db.execute("DELETE FROM amilk.amilks WHERE id = ?", (amilk_id,)) diff --git a/lnbits/extensions/amilk/migrations.py b/lnbits/extensions/amilk/migrations.py index f096ccdb..596a8633 100644 --- a/lnbits/extensions/amilk/migrations.py +++ b/lnbits/extensions/amilk/migrations.py @@ -4,7 +4,7 @@ async def m001_initial(db): """ await db.execute( """ - CREATE TABLE IF NOT EXISTS amilks ( + CREATE TABLE amilk.amilks ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, lnurl TEXT NOT NULL, diff --git a/lnbits/extensions/amilk/templates/amilk/index.html b/lnbits/extensions/amilk/templates/amilk/index.html index 357dd885..bb332e27 100644 --- a/lnbits/extensions/amilk/templates/amilk/index.html +++ b/lnbits/extensions/amilk/templates/amilk/index.html @@ -4,7 +4,7 @@
- New AMilk @@ -109,7 +109,7 @@ > Create amilk Optional[Bleskomat]: - row = await db.fetchone("SELECT * FROM bleskomats WHERE id = ?", (bleskomat_id,)) + row = await db.fetchone( + "SELECT * FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,) + ) return Bleskomat(**row) if row else None async def get_bleskomat_by_api_key_id(api_key_id: str) -> Optional[Bleskomat]: row = await db.fetchone( - "SELECT * FROM bleskomats WHERE api_key_id = ?", (api_key_id,) + "SELECT * FROM bleskomat.bleskomats WHERE api_key_id = ?", (api_key_id,) ) return Bleskomat(**row) if row else None @@ -58,7 +60,7 @@ async def get_bleskomats(wallet_ids: Union[str, List[str]]) -> List[Bleskomat]: wallet_ids = [wallet_ids] q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall( - f"SELECT * FROM bleskomats WHERE wallet IN ({q})", (*wallet_ids,) + f"SELECT * FROM bleskomat.bleskomats WHERE wallet IN ({q})", (*wallet_ids,) ) return [Bleskomat(**row) for row in rows] @@ -66,14 +68,17 @@ async def get_bleskomats(wallet_ids: Union[str, List[str]]) -> List[Bleskomat]: async def update_bleskomat(bleskomat_id: str, **kwargs) -> Optional[Bleskomat]: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( - f"UPDATE bleskomats SET {q} WHERE id = ?", (*kwargs.values(), bleskomat_id) + f"UPDATE bleskomat.bleskomats SET {q} WHERE id = ?", + (*kwargs.values(), bleskomat_id), + ) + row = await db.fetchone( + "SELECT * FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,) ) - row = await db.fetchone("SELECT * FROM bleskomats WHERE id = ?", (bleskomat_id,)) return Bleskomat(**row) if row else None async def delete_bleskomat(bleskomat_id: str) -> None: - await db.execute("DELETE FROM bleskomats WHERE id = ?", (bleskomat_id,)) + await db.execute("DELETE FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,)) async def create_bleskomat_lnurl( @@ -84,7 +89,7 @@ async def create_bleskomat_lnurl( now = int(time.time()) await db.execute( """ - INSERT INTO bleskomat_lnurls (id, bleskomat, wallet, hash, tag, params, api_key_id, initial_uses, remaining_uses, created_time, updated_time) + INSERT INTO bleskomat.bleskomat_lnurls (id, bleskomat, wallet, hash, tag, params, api_key_id, initial_uses, remaining_uses, created_time, updated_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( @@ -108,5 +113,7 @@ async def create_bleskomat_lnurl( async def get_bleskomat_lnurl(secret: str) -> Optional[BleskomatLnurl]: hash = generate_bleskomat_lnurl_hash(secret) - row = await db.fetchone("SELECT * FROM bleskomat_lnurls WHERE hash = ?", (hash,)) + row = await db.fetchone( + "SELECT * FROM bleskomat.bleskomat_lnurls WHERE hash = ?", (hash,) + ) return BleskomatLnurl(**row) if row else None diff --git a/lnbits/extensions/bleskomat/migrations.py b/lnbits/extensions/bleskomat/migrations.py index f7956500..84e886e5 100644 --- a/lnbits/extensions/bleskomat/migrations.py +++ b/lnbits/extensions/bleskomat/migrations.py @@ -2,7 +2,7 @@ async def m001_initial(db): await db.execute( """ - CREATE TABLE IF NOT EXISTS bleskomats ( + CREATE TABLE bleskomat.bleskomats ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, api_key_id TEXT NOT NULL, @@ -19,7 +19,7 @@ async def m001_initial(db): await db.execute( """ - CREATE TABLE IF NOT EXISTS bleskomat_lnurls ( + CREATE TABLE bleskomat.bleskomat_lnurls ( id TEXT PRIMARY KEY, bleskomat TEXT NOT NULL, wallet TEXT NOT NULL, diff --git a/lnbits/extensions/bleskomat/models.py b/lnbits/extensions/bleskomat/models.py index 54633146..216f83c6 100644 --- a/lnbits/extensions/bleskomat/models.py +++ b/lnbits/extensions/bleskomat/models.py @@ -100,7 +100,7 @@ class BleskomatLnurl(NamedTuple): now = int(time.time()) result = await conn.execute( """ - UPDATE bleskomat_lnurls + UPDATE bleskomat.bleskomat_lnurls SET remaining_uses = remaining_uses - 1, updated_time = ? WHERE id = ? AND remaining_uses > 0 diff --git a/lnbits/extensions/bleskomat/templates/bleskomat/index.html b/lnbits/extensions/bleskomat/templates/bleskomat/index.html index d00937c1..109626e3 100644 --- a/lnbits/extensions/bleskomat/templates/bleskomat/index.html +++ b/lnbits/extensions/bleskomat/templates/bleskomat/index.html @@ -11,7 +11,7 @@
- Add Bleskomat @@ -150,14 +150,14 @@ Update Bleskomat Optional[Captcha]: - row = await db.fetchone("SELECT * FROM captchas WHERE id = ?", (captcha_id,)) + row = await db.fetchone( + "SELECT * FROM captcha.captchas WHERE id = ?", (captcha_id,) + ) return Captcha.from_row(row) if row else None @@ -41,11 +43,11 @@ async def get_captchas(wallet_ids: Union[str, List[str]]) -> List[Captcha]: q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall( - f"SELECT * FROM captchas WHERE wallet IN ({q})", (*wallet_ids,) + f"SELECT * FROM captcha.captchas WHERE wallet IN ({q})", (*wallet_ids,) ) return [Captcha.from_row(row) for row in rows] async def delete_captcha(captcha_id: str) -> None: - await db.execute("DELETE FROM captchas WHERE id = ?", (captcha_id,)) + await db.execute("DELETE FROM captcha.captchas WHERE id = ?", (captcha_id,)) diff --git a/lnbits/extensions/captcha/migrations.py b/lnbits/extensions/captcha/migrations.py index 9fe2e604..744fc506 100644 --- a/lnbits/extensions/captcha/migrations.py +++ b/lnbits/extensions/captcha/migrations.py @@ -1,20 +1,19 @@ -from sqlalchemy.exc import OperationalError # type: ignore - - async def m001_initial(db): """ Initial captchas table. """ await db.execute( """ - CREATE TABLE IF NOT EXISTS captchas ( + CREATE TABLE captcha.captchas ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, secret TEXT NOT NULL, url TEXT NOT NULL, memo TEXT NOT NULL, amount INTEGER NOT NULL, - time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) + time TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ ); """ ) @@ -24,44 +23,41 @@ async def m002_redux(db): """ Creates an improved captchas table and migrates the existing data. """ - try: - await db.execute("SELECT remembers FROM captchas") + await db.execute("ALTER TABLE captcha.captchas RENAME TO captchas_old") + await db.execute( + """ + CREATE TABLE captcha.captchas ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + url TEXT NOT NULL, + memo TEXT NOT NULL, + description TEXT NULL, + amount INTEGER DEFAULT 0, + time TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """, + remembers INTEGER DEFAULT 0, + extras TEXT NULL + ); + """ + ) - except OperationalError: - await db.execute("ALTER TABLE captchas RENAME TO captchas_old") + for row in [ + list(row) for row in await db.fetchall("SELECT * FROM captcha.captchas_old") + ]: await db.execute( """ - CREATE TABLE IF NOT EXISTS captchas ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - url TEXT NOT NULL, - memo TEXT NOT NULL, - description TEXT NULL, - amount INTEGER DEFAULT 0, - time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')), - remembers INTEGER DEFAULT 0, - extras TEXT NULL - ); - """ - ) - await db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON captchas (wallet)") - - for row in [ - list(row) for row in await db.fetchall("SELECT * FROM captchas_old") - ]: - await db.execute( - """ - INSERT INTO captchas ( - id, - wallet, - url, - memo, - amount, - time - ) - VALUES (?, ?, ?, ?, ?, ?) - """, - (row[0], row[1], row[3], row[4], row[5], row[6]), + INSERT INTO captcha.captchas ( + id, + wallet, + url, + memo, + amount, + time ) + VALUES (?, ?, ?, ?, ?, ?) + """, + (row[0], row[1], row[3], row[4], row[5], row[6]), + ) - await db.execute("DROP TABLE captchas_old") + await db.execute("DROP TABLE captcha.captchas_old") diff --git a/lnbits/extensions/captcha/templates/captcha/display.html b/lnbits/extensions/captcha/templates/captcha/display.html index 80e59e63..a96cae05 100644 --- a/lnbits/extensions/captcha/templates/captcha/display.html +++ b/lnbits/extensions/captcha/templates/captcha/display.html @@ -24,7 +24,7 @@ dense flat icon="check" - color="deep-purple" + color="primary" type="submit" @click="createInvoice" :disabled="userAmount < captchaAmount || paymentReq" diff --git a/lnbits/extensions/captcha/templates/captcha/index.html b/lnbits/extensions/captcha/templates/captcha/index.html index 2250bced..d93a24f8 100644 --- a/lnbits/extensions/captcha/templates/captcha/index.html +++ b/lnbits/extensions/captcha/templates/captcha/index.html @@ -4,7 +4,7 @@
- New captcha @@ -141,7 +141,7 @@ @@ -157,7 +157,7 @@
Create captcha 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,)) diff --git a/lnbits/extensions/copilot/lnurl.py b/lnbits/extensions/copilot/lnurl.py new file mode 100644 index 00000000..0a10e29b --- /dev/null +++ b/lnbits/extensions/copilot/lnurl.py @@ -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/", 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/", 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()) diff --git a/lnbits/extensions/copilot/migrations.py b/lnbits/extensions/copilot/migrations.py new file mode 100644 index 00000000..e4149f56 --- /dev/null +++ b/lnbits/extensions/copilot/migrations.py @@ -0,0 +1,33 @@ +async def m001_initial(db): + """ + Initial copilot table. + """ + + await db.execute( + f""" + CREATE TABLE copilot.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 {db.timestamp_now} + ); + """ + ) diff --git a/lnbits/extensions/copilot/models.py b/lnbits/extensions/copilot/models.py new file mode 100644 index 00000000..70d70cf5 --- /dev/null +++ b/lnbits/extensions/copilot/models.py @@ -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) diff --git a/lnbits/extensions/copilot/static/bitcoin.gif b/lnbits/extensions/copilot/static/bitcoin.gif new file mode 100644 index 00000000..ef8c2ecd Binary files /dev/null and b/lnbits/extensions/copilot/static/bitcoin.gif differ diff --git a/lnbits/extensions/copilot/static/confetti.gif b/lnbits/extensions/copilot/static/confetti.gif new file mode 100644 index 00000000..a3fec971 Binary files /dev/null and b/lnbits/extensions/copilot/static/confetti.gif differ diff --git a/lnbits/extensions/copilot/static/face.gif b/lnbits/extensions/copilot/static/face.gif new file mode 100644 index 00000000..3e70d779 Binary files /dev/null and b/lnbits/extensions/copilot/static/face.gif differ diff --git a/lnbits/extensions/copilot/static/lnurl.png b/lnbits/extensions/copilot/static/lnurl.png new file mode 100644 index 00000000..ad2c9715 Binary files /dev/null and b/lnbits/extensions/copilot/static/lnurl.png differ diff --git a/lnbits/extensions/copilot/static/martijn.gif b/lnbits/extensions/copilot/static/martijn.gif new file mode 100644 index 00000000..e410677d Binary files /dev/null and b/lnbits/extensions/copilot/static/martijn.gif differ diff --git a/lnbits/extensions/copilot/static/rick.gif b/lnbits/extensions/copilot/static/rick.gif new file mode 100644 index 00000000..c36c7e19 Binary files /dev/null and b/lnbits/extensions/copilot/static/rick.gif differ diff --git a/lnbits/extensions/copilot/static/rocket.gif b/lnbits/extensions/copilot/static/rocket.gif new file mode 100644 index 00000000..6f19597d Binary files /dev/null and b/lnbits/extensions/copilot/static/rocket.gif differ diff --git a/lnbits/extensions/copilot/tasks.py b/lnbits/extensions/copilot/tasks.py new file mode 100644 index 00000000..ff291e9a --- /dev/null +++ b/lnbits/extensions/copilot/tasks.py @@ -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), + ) diff --git a/lnbits/extensions/copilot/templates/copilot/_api_docs.html b/lnbits/extensions/copilot/templates/copilot/_api_docs.html new file mode 100644 index 00000000..d6289be9 --- /dev/null +++ b/lnbits/extensions/copilot/templates/copilot/_api_docs.html @@ -0,0 +1,172 @@ + + +

+ StreamerCopilot: get tips via static QR (lnurl-pay) and show an + animation
+ + Created by, Ben Arc +

+ + + + + + POST /copilot/api/v1/copilot +
Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<copilot_object>, ...] +
Curl example
+ 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 }}" + +
+
+
+ + + + PUT + /copilot/api/v1/copilot/<copilot_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<copilot_object>, ...] +
Curl example
+ 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 }}" + +
+
+
+ + + + + GET + /copilot/api/v1/copilot/<copilot_id> +
Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<copilot_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/copilot/<copilot_id> + -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + GET /copilot/api/v1/copilots +
Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<copilot_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/copilots -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" + +
+
+
+ + + + DELETE + /copilot/api/v1/copilot/<copilot_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Returns 204 NO CONTENT
+ +
Curl example
+ curl -X DELETE {{ request.url_root + }}api/v1/copilot/<copilot_id> -H "X-Api-Key: {{ + g.user.wallets[0].adminkey }}" + +
+
+
+ + + + GET + /api/v1/copilot/ws/<copilot_id>/<comment>/<data> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Returns 200
+ +
Curl example
+ 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 }}" + +
+
+
+
+ diff --git a/lnbits/extensions/copilot/templates/copilot/compose.html b/lnbits/extensions/copilot/templates/copilot/compose.html new file mode 100644 index 00000000..33bffda3 --- /dev/null +++ b/lnbits/extensions/copilot/templates/copilot/compose.html @@ -0,0 +1,289 @@ +{% extends "public.html" %} {% block page %} + + + + +
+
+ +
+ {% raw %}{{ copilot.lnurl_title }}{% endraw %} +
+
+
+ +

+ {% raw %}{{ price }}{% endraw %} +

+

+ Powered by LNbits/StreamerCopilot +

+
+{% endblock %} {% block scripts %} + + + +{% endblock %} diff --git a/lnbits/extensions/copilot/templates/copilot/index.html b/lnbits/extensions/copilot/templates/copilot/index.html new file mode 100644 index 00000000..0e652e71 --- /dev/null +++ b/lnbits/extensions/copilot/templates/copilot/index.html @@ -0,0 +1,637 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + {% raw %} + New copilot instance + + + + + + +
+
+
Copilots
+
+ +
+ + + + Export to CSV +
+
+ + + + + {% endraw %} + +
+
+
+ +
+ + +
LNbits StreamCopilot Extension
+
+ + + {% include "copilot/_api_docs.html" %} + +
+
+ + + + +
+ +
+ +
+ + + + + + + +
+
+ +
+ +
+ + +
+
+ + +
+
+
+
+
+ + + + +
+
+ +
+ +
+ + +
+
+ + +
+
+
+
+
+ + + + +
+
+ +
+ +
+ + +
+
+ + +
+
+
+
+
+ + + +
+ +
+ +
+
+
+ +
+
+
+ Update Copilot + Create Copilot + Cancel +
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + + + +{% endblock %} diff --git a/lnbits/extensions/copilot/templates/copilot/panel.html b/lnbits/extensions/copilot/templates/copilot/panel.html new file mode 100644 index 00000000..904ab104 --- /dev/null +++ b/lnbits/extensions/copilot/templates/copilot/panel.html @@ -0,0 +1,157 @@ +{% extends "public.html" %} {% block page %} +
+ +
+
+
+ +
+
+
+
+ Title: {% raw %} {{ copilot.title }} {% endraw %} +
+
+ +
+
+
+ + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/lnbits/extensions/copilot/views.py b/lnbits/extensions/copilot/views.py new file mode 100644 index 00000000..ef313a61 --- /dev/null +++ b/lnbits/extensions/copilot/views.py @@ -0,0 +1,61 @@ +from quart import g, abort, render_template, jsonify, websocket +from http import HTTPStatus +import httpx +from collections import defaultdict +from lnbits.decorators import check_user_exists, validate_uuids +from . import copilot_ext +from .crud import get_copilot +from quart import g, abort, render_template, jsonify, websocket +from functools import wraps +import trio +import shortuuid +from . import copilot_ext + + +@copilot_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("copilot/index.html", user=g.user) + + +@copilot_ext.route("/cp/") +async def compose(): + return await render_template("copilot/compose.html") + + +@copilot_ext.route("/pn/") +async def panel(): + return await render_template("copilot/panel.html") + + +##################WEBSOCKET ROUTES######################## + +# socket_relay is a list where the control panel or +# lnurl endpoints can leave a message for the compose window + +connected_websockets = defaultdict(set) + + +@copilot_ext.websocket("/ws//") +async def wss(id): + copilot = await get_copilot(id) + if not copilot: + return "", HTTPStatus.FORBIDDEN + global connected_websockets + send_channel, receive_channel = trio.open_memory_channel(0) + connected_websockets[id].add(send_channel) + try: + while True: + data = await receive_channel.receive() + await websocket.send(data) + finally: + connected_websockets[id].remove(send_channel) + + +async def updater(copilot_id, data, comment): + copilot = await get_copilot(copilot_id) + if not copilot: + return + for queue in connected_websockets[copilot_id]: + await queue.send(f"{data + '-' + comment}") diff --git a/lnbits/extensions/copilot/views_api.py b/lnbits/extensions/copilot/views_api.py new file mode 100644 index 00000000..bf3b4eb7 --- /dev/null +++ b/lnbits/extensions/copilot/views_api.py @@ -0,0 +1,109 @@ +import hashlib +from quart import g, jsonify, url_for, websocket +from http import HTTPStatus +import httpx + +from lnbits.core.crud import get_user +from lnbits.decorators import api_check_wallet_key, api_validate_post_request +from .views import updater + +from . import copilot_ext + +from lnbits.extensions.copilot import copilot_ext +from .crud import ( + create_copilot, + update_copilot, + get_copilot, + get_copilots, + delete_copilot, +) + +#######################COPILOT########################## + + +@copilot_ext.route("/api/v1/copilot", methods=["POST"]) +@copilot_ext.route("/api/v1/copilot/", methods=["PUT"]) +@api_check_wallet_key("admin") +@api_validate_post_request( + schema={ + "title": {"type": "string", "empty": False, "required": True}, + "lnurl_toggle": {"type": "integer", "empty": False}, + "wallet": {"type": "string", "empty": False, "required": False}, + "animation1": {"type": "string", "empty": True, "required": False}, + "animation2": {"type": "string", "empty": True, "required": False}, + "animation3": {"type": "string", "empty": True, "required": False}, + "animation1threshold": {"type": "integer", "empty": True, "required": False}, + "animation2threshold": {"type": "integer", "empty": True, "required": False}, + "animation3threshold": {"type": "integer", "empty": True, "required": False}, + "animation1webhook": {"type": "string", "empty": True, "required": False}, + "animation2webhook": {"type": "string", "empty": True, "required": False}, + "animation3webhook": {"type": "string", "empty": True, "required": False}, + "lnurl_title": {"type": "string", "empty": True, "required": False}, + "show_message": {"type": "integer", "empty": True, "required": False}, + "show_ack": {"type": "integer", "empty": True}, + "show_price": {"type": "string", "empty": True}, + } +) +async def api_copilot_create_or_update(copilot_id=None): + if not copilot_id: + copilot = await create_copilot(user=g.wallet.user, **g.data) + return jsonify(copilot._asdict()), HTTPStatus.CREATED + else: + copilot = await update_copilot(copilot_id=copilot_id, **g.data) + return jsonify(copilot._asdict()), HTTPStatus.OK + + +@copilot_ext.route("/api/v1/copilot", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_copilots_retrieve(): + try: + return ( + jsonify( + [{**copilot._asdict()} for copilot in await get_copilots(g.wallet.user)] + ), + HTTPStatus.OK, + ) + except: + return "" + + +@copilot_ext.route("/api/v1/copilot/", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_copilot_retrieve(copilot_id): + copilot = await get_copilot(copilot_id) + if not copilot: + return jsonify({"message": "copilot does not exist"}), HTTPStatus.NOT_FOUND + if not copilot.lnurl_toggle: + return ( + jsonify({**copilot._asdict()}), + HTTPStatus.OK, + ) + return ( + jsonify({**copilot._asdict(), **{"lnurl": copilot.lnurl}}), + HTTPStatus.OK, + ) + + +@copilot_ext.route("/api/v1/copilot/", methods=["DELETE"]) +@api_check_wallet_key("admin") +async def api_copilot_delete(copilot_id): + copilot = await get_copilot(copilot_id) + + if not copilot: + return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND + + await delete_copilot(copilot_id) + + return "", HTTPStatus.NO_CONTENT + + +@copilot_ext.route("/api/v1/copilot/ws///", methods=["GET"]) +async def api_copilot_ws_relay(copilot_id, comment, data): + copilot = await get_copilot(copilot_id) + if not copilot: + return jsonify({"message": "copilot does not exist"}), HTTPStatus.NOT_FOUND + try: + await updater(copilot_id, data, comment) + except: + return "", HTTPStatus.FORBIDDEN + return "", HTTPStatus.OK diff --git a/lnbits/extensions/diagonalley/crud.py b/lnbits/extensions/diagonalley/crud.py index 8e89c35a..971cd449 100644 --- a/lnbits/extensions/diagonalley/crud.py +++ b/lnbits/extensions/diagonalley/crud.py @@ -34,7 +34,7 @@ def create_diagonalleys_product( product_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8") db.execute( """ - INSERT INTO products (id, wallet, product, categories, description, image, price, quantity) + INSERT INTO diagonalley.products (id, wallet, product, categories, description, image, price, quantity) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( @@ -57,16 +57,21 @@ def update_diagonalleys_product(product_id: str, **kwargs) -> Optional[Indexers] with open_ext_db("diagonalley") as db: db.execute( - f"UPDATE products SET {q} WHERE id = ?", (*kwargs.values(), product_id) + f"UPDATE diagonalley.products SET {q} WHERE id = ?", + (*kwargs.values(), product_id), + ) + row = db.fetchone( + "SELECT * FROM diagonalley.products WHERE id = ?", (product_id,) ) - row = db.fetchone("SELECT * FROM products WHERE id = ?", (product_id,)) return get_diagonalleys_indexer(product_id) def get_diagonalleys_product(product_id: str) -> Optional[Products]: with open_ext_db("diagonalley") as db: - row = db.fetchone("SELECT * FROM products WHERE id = ?", (product_id,)) + row = db.fetchone( + "SELECT * FROM diagonalley.products WHERE id = ?", (product_id,) + ) return Products(**row) if row else None @@ -78,7 +83,7 @@ def get_diagonalleys_products(wallet_ids: Union[str, List[str]]) -> List[Product with open_ext_db("diagonalley") as db: q = ",".join(["?"] * len(wallet_ids)) rows = db.fetchall( - f"SELECT * FROM products WHERE wallet IN ({q})", (*wallet_ids,) + f"SELECT * FROM diagonalley.products WHERE wallet IN ({q})", (*wallet_ids,) ) return [Products(**row) for row in rows] @@ -86,7 +91,7 @@ def get_diagonalleys_products(wallet_ids: Union[str, List[str]]) -> List[Product def delete_diagonalleys_product(product_id: str) -> None: with open_ext_db("diagonalley") as db: - db.execute("DELETE FROM products WHERE id = ?", (product_id,)) + db.execute("DELETE FROM diagonalley.products WHERE id = ?", (product_id,)) ###Indexers @@ -106,7 +111,7 @@ def create_diagonalleys_indexer( indexer_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8") db.execute( """ - INSERT INTO indexers (id, wallet, shopname, indexeraddress, online, rating, shippingzone1, shippingzone2, zone1cost, zone2cost, email) + INSERT INTO diagonalley.indexers (id, wallet, shopname, indexeraddress, online, rating, shippingzone1, shippingzone2, zone1cost, zone2cost, email) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( @@ -131,16 +136,21 @@ def update_diagonalleys_indexer(indexer_id: str, **kwargs) -> Optional[Indexers] with open_ext_db("diagonalley") as db: db.execute( - f"UPDATE indexers SET {q} WHERE id = ?", (*kwargs.values(), indexer_id) + f"UPDATE diagonalley.indexers SET {q} WHERE id = ?", + (*kwargs.values(), indexer_id), + ) + row = db.fetchone( + "SELECT * FROM diagonalley.indexers WHERE id = ?", (indexer_id,) ) - row = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,)) return get_diagonalleys_indexer(indexer_id) def get_diagonalleys_indexer(indexer_id: str) -> Optional[Indexers]: with open_ext_db("diagonalley") as db: - roww = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,)) + roww = db.fetchone( + "SELECT * FROM diagonalley.indexers WHERE id = ?", (indexer_id,) + ) try: x = httpx.get(roww["indexeraddress"] + "/" + roww["ratingkey"]) if x.status_code == 200: @@ -148,7 +158,7 @@ def get_diagonalleys_indexer(indexer_id: str) -> Optional[Indexers]: print("poo") with open_ext_db("diagonalley") as db: db.execute( - "UPDATE indexers SET online = ? WHERE id = ?", + "UPDATE diagonalley.indexers SET online = ? WHERE id = ?", ( True, indexer_id, @@ -157,7 +167,7 @@ def get_diagonalleys_indexer(indexer_id: str) -> Optional[Indexers]: else: with open_ext_db("diagonalley") as db: db.execute( - "UPDATE indexers SET online = ? WHERE id = ?", + "UPDATE diagonalley.indexers SET online = ? WHERE id = ?", ( False, indexer_id, @@ -166,7 +176,9 @@ def get_diagonalleys_indexer(indexer_id: str) -> Optional[Indexers]: except: print("An exception occurred") with open_ext_db("diagonalley") as db: - row = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,)) + row = db.fetchone( + "SELECT * FROM diagonalley.indexers WHERE id = ?", (indexer_id,) + ) return Indexers(**row) if row else None @@ -177,7 +189,7 @@ def get_diagonalleys_indexers(wallet_ids: Union[str, List[str]]) -> List[Indexer with open_ext_db("diagonalley") as db: q = ",".join(["?"] * len(wallet_ids)) rows = db.fetchall( - f"SELECT * FROM indexers WHERE wallet IN ({q})", (*wallet_ids,) + f"SELECT * FROM diagonalley.indexers WHERE wallet IN ({q})", (*wallet_ids,) ) for r in rows: @@ -186,7 +198,7 @@ def get_diagonalleys_indexers(wallet_ids: Union[str, List[str]]) -> List[Indexer if x.status_code == 200: with open_ext_db("diagonalley") as db: db.execute( - "UPDATE indexers SET online = ? WHERE id = ?", + "UPDATE diagonalley.indexers SET online = ? WHERE id = ?", ( True, r["id"], @@ -195,7 +207,7 @@ def get_diagonalleys_indexers(wallet_ids: Union[str, List[str]]) -> List[Indexer else: with open_ext_db("diagonalley") as db: db.execute( - "UPDATE indexers SET online = ? WHERE id = ?", + "UPDATE diagonalley.indexers SET online = ? WHERE id = ?", ( False, r["id"], @@ -206,14 +218,14 @@ def get_diagonalleys_indexers(wallet_ids: Union[str, List[str]]) -> List[Indexer with open_ext_db("diagonalley") as db: q = ",".join(["?"] * len(wallet_ids)) rows = db.fetchall( - f"SELECT * FROM indexers WHERE wallet IN ({q})", (*wallet_ids,) + f"SELECT * FROM diagonalley.indexers WHERE wallet IN ({q})", (*wallet_ids,) ) return [Indexers(**row) for row in rows] def delete_diagonalleys_indexer(indexer_id: str) -> None: with open_ext_db("diagonalley") as db: - db.execute("DELETE FROM indexers WHERE id = ?", (indexer_id,)) + db.execute("DELETE FROM diagonalley.indexers WHERE id = ?", (indexer_id,)) ###Orders @@ -236,7 +248,7 @@ def create_diagonalleys_order( order_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8") db.execute( """ - INSERT INTO orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped) + INSERT INTO diagonalley.orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( @@ -259,7 +271,7 @@ def create_diagonalleys_order( def get_diagonalleys_order(order_id: str) -> Optional[Orders]: with open_ext_db("diagonalley") as db: - row = db.fetchone("SELECT * FROM orders WHERE id = ?", (order_id,)) + row = db.fetchone("SELECT * FROM diagonalley.orders WHERE id = ?", (order_id,)) return Orders(**row) if row else None @@ -271,25 +283,26 @@ def get_diagonalleys_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]: with open_ext_db("diagonalley") as db: q = ",".join(["?"] * len(wallet_ids)) rows = db.fetchall( - f"SELECT * FROM orders WHERE wallet IN ({q})", (*wallet_ids,) + f"SELECT * FROM diagonalley.orders WHERE wallet IN ({q})", (*wallet_ids,) ) for r in rows: PAID = (await WALLET.get_invoice_status(r["invoiceid"])).paid if PAID: with open_ext_db("diagonalley") as db: db.execute( - "UPDATE orders SET paid = ? WHERE id = ?", + "UPDATE diagonalley.orders SET paid = ? WHERE id = ?", ( True, r["id"], ), ) rows = db.fetchall( - f"SELECT * FROM orders WHERE wallet IN ({q})", (*wallet_ids,) + f"SELECT * FROM diagonalley.orders WHERE wallet IN ({q})", + (*wallet_ids,), ) return [Orders(**row) for row in rows] def delete_diagonalleys_order(order_id: str) -> None: with open_ext_db("diagonalley") as db: - db.execute("DELETE FROM orders WHERE id = ?", (order_id,)) + db.execute("DELETE FROM diagonalley.orders WHERE id = ?", (order_id,)) diff --git a/lnbits/extensions/diagonalley/migrations.py b/lnbits/extensions/diagonalley/migrations.py index a70368fc..9f2b787f 100644 --- a/lnbits/extensions/diagonalley/migrations.py +++ b/lnbits/extensions/diagonalley/migrations.py @@ -4,7 +4,7 @@ async def m001_initial(db): """ await db.execute( """ - CREATE TABLE IF NOT EXISTS products ( + CREATE TABLE diagonalley.products ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, product TEXT NOT NULL, @@ -22,7 +22,7 @@ async def m001_initial(db): """ await db.execute( """ - CREATE TABLE IF NOT EXISTS indexers ( + CREATE TABLE diagonalley.indexers ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, shopname TEXT NOT NULL, @@ -43,7 +43,7 @@ async def m001_initial(db): """ await db.execute( """ - CREATE TABLE IF NOT EXISTS orders ( + CREATE TABLE diagonalley.orders ( id TEXT PRIMARY KEY, productid TEXT NOT NULL, wallet TEXT NOT NULL, diff --git a/lnbits/extensions/diagonalley/templates/diagonalley/index.html b/lnbits/extensions/diagonalley/templates/diagonalley/index.html index cdf9da53..c041239f 100644 --- a/lnbits/extensions/diagonalley/templates/diagonalley/index.html +++ b/lnbits/extensions/diagonalley/templates/diagonalley/index.html @@ -4,10 +4,10 @@
- New Product - New Indexer Frontend shop your stall will list its products in @@ -282,7 +282,7 @@ Update Product @@ -290,7 +290,7 @@ Update Indexer @@ -382,7 +382,7 @@ Tickets: await db.execute( """ - INSERT INTO ticket (id, wallet, event, name, email, registered, paid) + INSERT INTO events.ticket (id, wallet, event, name, email, registered, paid) VALUES (?, ?, ?, ?, ?, ?, ?) """, (payment_hash, wallet, event, name, email, False, False), @@ -26,11 +26,11 @@ async def create_ticket( async def set_ticket_paid(payment_hash: str) -> Tickets: - row = await db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,)) + row = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (payment_hash,)) if row[6] != True: await db.execute( """ - UPDATE ticket + UPDATE events.ticket SET paid = true WHERE id = ? """, @@ -44,7 +44,7 @@ async def set_ticket_paid(payment_hash: str) -> Tickets: amount_tickets = eventdata.amount_tickets - 1 await db.execute( """ - UPDATE events + UPDATE events.events SET sold = ?, amount_tickets = ? WHERE id = ? """, @@ -57,7 +57,7 @@ async def set_ticket_paid(payment_hash: str) -> Tickets: async def get_ticket(payment_hash: str) -> Optional[Tickets]: - row = await db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,)) + row = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (payment_hash,)) return Tickets(**row) if row else None @@ -67,13 +67,13 @@ async def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]: q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall( - f"SELECT * FROM ticket WHERE wallet IN ({q})", (*wallet_ids,) + f"SELECT * FROM events.ticket WHERE wallet IN ({q})", (*wallet_ids,) ) return [Tickets(**row) for row in rows] async def delete_ticket(payment_hash: str) -> None: - await db.execute("DELETE FROM ticket WHERE id = ?", (payment_hash,)) + await db.execute("DELETE FROM events.ticket WHERE id = ?", (payment_hash,)) # EVENTS @@ -93,7 +93,7 @@ async def create_event( event_id = urlsafe_short_hash() await db.execute( """ - INSERT INTO events (id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold) + INSERT INTO events.events (id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( @@ -118,7 +118,7 @@ async def create_event( async def update_event(event_id: str, **kwargs) -> Events: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( - f"UPDATE events SET {q} WHERE id = ?", (*kwargs.values(), event_id) + f"UPDATE events.events SET {q} WHERE id = ?", (*kwargs.values(), event_id) ) event = await get_event(event_id) assert event, "Newly updated event couldn't be retrieved" @@ -126,7 +126,7 @@ async def update_event(event_id: str, **kwargs) -> Events: async def get_event(event_id: str) -> Optional[Events]: - row = await db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,)) + row = await db.fetchone("SELECT * FROM events.events WHERE id = ?", (event_id,)) return Events(**row) if row else None @@ -136,14 +136,14 @@ async def get_events(wallet_ids: Union[str, List[str]]) -> List[Events]: q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall( - f"SELECT * FROM events WHERE wallet IN ({q})", (*wallet_ids,) + f"SELECT * FROM events.events WHERE wallet IN ({q})", (*wallet_ids,) ) return [Events(**row) for row in rows] async def delete_event(event_id: str) -> None: - await db.execute("DELETE FROM events WHERE id = ?", (event_id,)) + await db.execute("DELETE FROM events.events WHERE id = ?", (event_id,)) # EVENTTICKETS @@ -151,13 +151,18 @@ async def delete_event(event_id: str) -> None: async def get_event_tickets(event_id: str, wallet_id: str) -> List[Tickets]: rows = await db.fetchall( - "SELECT * FROM ticket WHERE wallet = ? AND event = ?", (wallet_id, event_id) + "SELECT * FROM events.ticket WHERE wallet = ? AND event = ?", + (wallet_id, event_id), ) return [Tickets(**row) for row in rows] async def reg_ticket(ticket_id: str) -> List[Tickets]: - await db.execute("UPDATE ticket SET registered = ? WHERE id = ?", (True, ticket_id)) - ticket = await db.fetchone("SELECT * FROM ticket WHERE id = ?", (ticket_id,)) - rows = await db.fetchall("SELECT * FROM ticket WHERE event = ?", (ticket[1],)) + await db.execute( + "UPDATE events.ticket SET registered = ? WHERE id = ?", (True, ticket_id) + ) + ticket = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (ticket_id,)) + rows = await db.fetchall( + "SELECT * FROM events.ticket WHERE event = ?", (ticket[1],) + ) return [Tickets(**row) for row in rows] diff --git a/lnbits/extensions/events/migrations.py b/lnbits/extensions/events/migrations.py index 5d0e5840..d8f3d94e 100644 --- a/lnbits/extensions/events/migrations.py +++ b/lnbits/extensions/events/migrations.py @@ -2,7 +2,7 @@ async def m001_initial(db): await db.execute( """ - CREATE TABLE IF NOT EXISTS events ( + CREATE TABLE events.events ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, name TEXT NOT NULL, @@ -13,21 +13,25 @@ async def m001_initial(db): amount_tickets INTEGER NOT NULL, price_per_ticket INTEGER NOT NULL, sold INTEGER NOT NULL, - time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) + time TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ ); """ ) await db.execute( """ - CREATE TABLE IF NOT EXISTS tickets ( + CREATE TABLE events.tickets ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, event TEXT NOT NULL, name TEXT NOT NULL, email TEXT NOT NULL, registered BOOLEAN NOT NULL, - time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) + time TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ ); """ ) @@ -37,7 +41,7 @@ async def m002_changed(db): await db.execute( """ - CREATE TABLE IF NOT EXISTS ticket ( + CREATE TABLE events.ticket ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, event TEXT NOT NULL, @@ -45,12 +49,14 @@ async def m002_changed(db): email TEXT NOT NULL, registered BOOLEAN NOT NULL, paid BOOLEAN NOT NULL, - time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) + time TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ ); """ ) - for row in [list(row) for row in await db.fetchall("SELECT * FROM tickets")]: + for row in [list(row) for row in await db.fetchall("SELECT * FROM events.tickets")]: usescsv = "" for i in range(row[5]): @@ -61,7 +67,7 @@ async def m002_changed(db): usescsv = usescsv[1:] await db.execute( """ - INSERT INTO ticket ( + INSERT INTO events.ticket ( id, wallet, event, @@ -82,4 +88,4 @@ async def m002_changed(db): True, ), ) - await db.execute("DROP TABLE tickets") + await db.execute("DROP TABLE events.tickets") diff --git a/lnbits/extensions/events/templates/events/display.html b/lnbits/extensions/events/templates/events/display.html index be10b08c..4c1f557f 100644 --- a/lnbits/extensions/events/templates/events/display.html +++ b/lnbits/extensions/events/templates/events/display.html @@ -26,7 +26,7 @@
SubmitLink to your ticket! diff --git a/lnbits/extensions/events/templates/events/index.html b/lnbits/extensions/events/templates/events/index.html index 384cf630..5d3cc262 100644 --- a/lnbits/extensions/events/templates/events/index.html +++ b/lnbits/extensions/events/templates/events/index.html @@ -4,7 +4,7 @@
- New Event @@ -267,14 +267,14 @@ Update Event Create Event - Scan ticket @@ -82,7 +82,7 @@ +{% endblock %} diff --git a/lnbits/extensions/hivemind/views.py b/lnbits/extensions/hivemind/views.py new file mode 100644 index 00000000..21c4c287 --- /dev/null +++ b/lnbits/extensions/hivemind/views.py @@ -0,0 +1,12 @@ +from quart import g, render_template + +from lnbits.decorators import check_user_exists, validate_uuids + +from . import hivemind_ext + + +@hivemind_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("hivemind/index.html", user=g.user) diff --git a/lnbits/extensions/jukebox/README.md b/lnbits/extensions/jukebox/README.md index b92e7ea6..c761db44 100644 --- a/lnbits/extensions/jukebox/README.md +++ b/lnbits/extensions/jukebox/README.md @@ -1,5 +1,36 @@ # Jukebox -To use this extension you need a Spotify client ID and client secret. You get these by creating an app in the Spotify developers dashboard here https://developer.spotify.com/dashboard/applications +## An actual Jukebox where users pay sats to play their favourite music from your playlists -Select the playlists you want people to be able to pay for, share the frontend page, profit :) +**Note:** To use this extension you need a Premium Spotify subscription. + +## Usage + +1. Click on "ADD SPOTIFY JUKEBOX"\ + ![add jukebox](https://i.imgur.com/NdVoKXd.png) +2. Follow the steps required on the form\ + + - give your jukebox a name + - select a wallet to receive payment + - define the price a user must pay to select a song\ + ![pick wallet price](https://i.imgur.com/4bJ8mb9.png) + - follow the steps to get your Spotify App and get the client ID and secret key\ + ![spotify keys](https://i.imgur.com/w2EzFtB.png) + - paste the codes in the form\ + ![api keys](https://i.imgur.com/6b9xauo.png) + - copy the _Redirect URL_ presented on the form\ + ![redirect url](https://i.imgur.com/GMzl0lG.png) + - on Spotify click the "EDIT SETTINGS" button and paste the copied link in the _Redirect URI's_ prompt + ![spotify app setting](https://i.imgur.com/vb0x4Tl.png) + - back on LNBits, click "AUTORIZE ACCESS" and "Agree" on the page that will open + - choose on which device the LNBits Jukebox extensions will stream to, you may have to be logged in in order to select the device (browser, smartphone app, etc...) + - and select what playlist will be available for users to choose songs (you need to have already playlist on Spotify)\ + ![select playlists](https://i.imgur.com/g4dbtED.png) + +3. After Jukebox is created, click the icon to open the dialog with the shareable QR, open the Jukebox page, etc...\ + ![shareable jukebox](https://i.imgur.com/EAh9PI0.png) +4. The users will see the Jukebox page and choose a song from the selected playlist\ + ![select song](https://i.imgur.com/YYjeQAs.png) +5. After selecting a song they'd like to hear next a dialog will show presenting the music\ + ![play for sats](https://i.imgur.com/eEHl3o8.png) +6. After payment, the song will automatically start playing on the device selected or enter the queue if some other music is already playing diff --git a/lnbits/extensions/jukebox/__init__.py b/lnbits/extensions/jukebox/__init__.py index b6ec402f..076ae4d9 100644 --- a/lnbits/extensions/jukebox/__init__.py +++ b/lnbits/extensions/jukebox/__init__.py @@ -10,3 +10,8 @@ jukebox_ext: Blueprint = Blueprint( from .views_api import * # noqa from .views import * # noqa +from .tasks import register_listeners + +from lnbits.tasks import record_async + +jukebox_ext.record(record_async(register_listeners)) diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py index a24d8a7e..4e3ba2f1 100644 --- a/lnbits/extensions/jukebox/crud.py +++ b/lnbits/extensions/jukebox/crud.py @@ -21,7 +21,7 @@ async def create_jukebox( juke_id = urlsafe_short_hash() result = await db.execute( """ - INSERT INTO jukebox (id, user, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit) + INSERT INTO jukebox.jukebox (id, user, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( @@ -47,35 +47,35 @@ async def create_jukebox( async def update_jukebox(juke_id: str, **kwargs) -> Optional[Jukebox]: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( - f"UPDATE jukebox SET {q} WHERE id = ?", (*kwargs.values(), juke_id) + f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (*kwargs.values(), juke_id) ) - row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (juke_id,)) + row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,)) return Jukebox(**row) if row else None async def get_jukebox(juke_id: str) -> Optional[Jukebox]: - row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (juke_id,)) + row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,)) return Jukebox(**row) if row else None async def get_jukebox_by_user(user: str) -> Optional[Jukebox]: - row = await db.fetchone("SELECT * FROM jukebox WHERE sp_user = ?", (user,)) + row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE sp_user = ?", (user,)) return Jukebox(**row) if row else None async def get_jukeboxs(user: str) -> List[Jukebox]: - rows = await db.fetchall("SELECT * FROM jukebox WHERE user = ?", (user,)) + rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,)) for row in rows: if row.sp_playlists == "": await delete_jukebox(row.id) - rows = await db.fetchall("SELECT * FROM jukebox WHERE user = ?", (user,)) + rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,)) return [Jukebox.from_row(row) for row in rows] async def delete_jukebox(juke_id: str): await db.execute( """ - DELETE FROM jukebox WHERE id = ? + DELETE FROM jukebox.jukebox WHERE id = ? """, (juke_id), ) @@ -89,7 +89,7 @@ async def create_jukebox_payment( ) -> JukeboxPayment: result = await db.execute( """ - INSERT INTO jukebox_payment (payment_hash, juke_id, song_id, paid) + INSERT INTO jukebox.jukebox_payment (payment_hash, juke_id, song_id, paid) VALUES (?, ?, ?, ?) """, ( @@ -109,7 +109,7 @@ async def update_jukebox_payment( ) -> Optional[JukeboxPayment]: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( - f"UPDATE jukebox_payment SET {q} WHERE payment_hash = ?", + f"UPDATE jukebox.jukebox_payment SET {q} WHERE payment_hash = ?", (*kwargs.values(), payment_hash), ) return await get_jukebox_payment(payment_hash) @@ -117,6 +117,6 @@ async def update_jukebox_payment( async def get_jukebox_payment(payment_hash: str) -> Optional[JukeboxPayment]: row = await db.fetchone( - "SELECT * FROM jukebox_payment WHERE payment_hash = ?", (payment_hash,) + "SELECT * FROM jukebox.jukebox_payment WHERE payment_hash = ?", (payment_hash,) ) return JukeboxPayment(**row) if row else None diff --git a/lnbits/extensions/jukebox/migrations.py b/lnbits/extensions/jukebox/migrations.py index 7d4fe2e3..a0a3bd28 100644 --- a/lnbits/extensions/jukebox/migrations.py +++ b/lnbits/extensions/jukebox/migrations.py @@ -4,9 +4,9 @@ async def m001_initial(db): """ await db.execute( """ - CREATE TABLE jukebox ( + CREATE TABLE jukebox.jukebox ( id TEXT PRIMARY KEY, - user TEXT, + "user" TEXT, title TEXT, wallet TEXT, inkey TEXT, @@ -29,7 +29,7 @@ async def m002_initial(db): """ await db.execute( """ - CREATE TABLE jukebox_payment ( + CREATE TABLE jukebox.jukebox_payment ( payment_hash TEXT PRIMARY KEY, juke_id TEXT, song_id TEXT, diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js index 57f9c678..fc382d71 100644 --- a/lnbits/extensions/jukebox/static/js/index.js +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -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( diff --git a/lnbits/extensions/jukebox/static/js/jukebox.js b/lnbits/extensions/jukebox/static/js/jukebox.js index b6e26f13..ddbb2764 100644 --- a/lnbits/extensions/jukebox/static/js/jukebox.js +++ b/lnbits/extensions/jukebox/static/js/jukebox.js @@ -6,14 +6,9 @@ new Vue({ el: '#vue', mixins: [windowMixin], data() { - return { - } + return {} }, computed: {}, - methods: { - - }, - created() { - - } + methods: {}, + created() {} }) diff --git a/lnbits/extensions/jukebox/tasks.py b/lnbits/extensions/jukebox/tasks.py new file mode 100644 index 00000000..65fca93d --- /dev/null +++ b/lnbits/extensions/jukebox/tasks.py @@ -0,0 +1,28 @@ +import json +import trio # type: ignore + +from lnbits.core.models import Payment +from lnbits.core.crud import create_payment +from lnbits.core import db as core_db +from lnbits.tasks import register_invoice_listener, internal_invoice_paid +from lnbits.helpers import urlsafe_short_hash + +from .crud import get_jukebox, update_jukebox_payment + + +async def register_listeners(): + invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2) + register_invoice_listener(invoice_paid_chan_send) + await wait_for_paid_invoices(invoice_paid_chan_recv) + + +async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): + async for payment in invoice_paid_chan: + await on_invoice_paid(payment) + + +async def on_invoice_paid(payment: Payment) -> None: + if "jukebox" != payment.extra.get("tag"): + # not a jukebox invoice + return + await update_jukebox_payment(payment.payment_hash, paid=True) diff --git a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html index 66c2dfcf..f5a91313 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html +++ b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html @@ -1,24 +1,33 @@ - To use this extension you need a Spotify client ID and client secret. You - get these by creating an app in the Spotify developers dashboard - here -

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

- Made by, benarc. Inspired by, - pirosb3. + To use this extension you need a Spotify client ID and client secret. You get + these by creating an app in the Spotify developers dashboard + here + +

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

+ Made by, + benarc. + Inspired by, + pirosb3. - - - - - - + - GET - /jukebox/api/v1/jukebox + GET /jukebox/api/v1/jukebox
Headers
{"X-Api-Key": <admin_key>}
Body (application/json)
@@ -27,7 +36,8 @@ [<jukebox_object>, ...]
Curl example
- curl -X GET {{ request.url_root }}api/v1/jukebox -H "X-Api-Key: {{ + curl -X GET {{ request.url_root }}api/v1/jukebox -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
@@ -36,8 +46,10 @@ - GET - /jukebox/api/v1/jukebox/<juke_id> + GET + /jukebox/api/v1/jukebox/<juke_id>
Headers
{"X-Api-Key": <admin_key>}
Body (application/json)
@@ -46,36 +58,44 @@ <jukebox_object>
Curl example
- curl -X GET {{ request.url_root }}api/v1/jukebox/<juke_id> -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" + curl -X GET {{ request.url_root }}api/v1/jukebox/<juke_id> -H + "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
- + - POST/PUT - /jukebox/api/v1/jukebox/ + POST/PUT + /jukebox/api/v1/jukebox/
Headers
{"X-Api-Key": <admin_key>}
-
- Body (application/json) -
+
Body (application/json)
Returns 200 OK (application/json)
<jukbox_object>
Curl example
- curl -X POST {{ request.url_root }}api/v1/jukebox/ -d - '{"user": <string, user_id>, - "title": <string>, "wallet":<string>, "sp_user": - <string, spotify_user_account>, "sp_secret": <string, spotify_user_secret>, "sp_access_token": - <string, not_required>, "sp_refresh_token": - <string, not_required>, "sp_device": <string, spotify_user_secret>, "sp_playlists": - <string, not_required>, "price": - <integer, not_required>}' -H "Content-type: - application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}" + 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 }}"
@@ -83,8 +103,10 @@ - DELETE - /jukebox/api/v1/jukebox/<juke_id> + DELETE + /jukebox/api/v1/jukebox/<juke_id>
Headers
{"X-Api-Key": <admin_key>}
Body (application/json)
@@ -93,9 +115,11 @@ <jukebox_object>
Curl example
- curl -X DELETE {{ request.url_root }}api/v1/jukebox/<juke_id> -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" + curl -X DELETE {{ request.url_root }}api/v1/jukebox/<juke_id> + -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
-
\ No newline at end of file +
diff --git a/lnbits/extensions/jukebox/templates/jukebox/error.html b/lnbits/extensions/jukebox/templates/jukebox/error.html index 746e403c..f6f7fd58 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/error.html +++ b/lnbits/extensions/jukebox/templates/jukebox/error.html @@ -12,7 +12,9 @@ style="font-size: 20rem" > -
Ask the host to turn on the device and launch spotify
+
+ Ask the host to turn on the device and launch spotify +

diff --git a/lnbits/extensions/jukebox/templates/jukebox/index.html b/lnbits/extensions/jukebox/templates/jukebox/index.html index 25cc49e5..ee572111 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/index.html +++ b/lnbits/extensions/jukebox/templates/jukebox/index.html @@ -4,18 +4,36 @@
- Add Spotify Jukebox + Add Spotify Jukebox {% raw %} - +