Merge branch 'master' into TwitchAlerts
19
.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
|
||||
|
|
1
.gitignore
vendored
|
@ -6,6 +6,7 @@ __pycache__
|
|||
*$py.class
|
||||
.mypy_cache
|
||||
.vscode
|
||||
*-lock.json
|
||||
|
||||
*.egg
|
||||
*.egg-info
|
||||
|
|
2
Pipfile
|
@ -17,7 +17,6 @@ shortuuid = "*"
|
|||
quart = "*"
|
||||
quart-cors = "*"
|
||||
quart-compress = "*"
|
||||
secure = "*"
|
||||
typing-extensions = "*"
|
||||
httpx = "*"
|
||||
quart-trio = "*"
|
||||
|
@ -35,3 +34,4 @@ pytest = "*"
|
|||
pytest-cov = "*"
|
||||
mypy = "latest"
|
||||
pytest-trio = "*"
|
||||
trio-typing = "*"
|
||||
|
|
405
Pipfile.lock
generated
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,13 @@ Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Ne
|
|||
## Running the server
|
||||
|
||||
LNbits uses [Quart][quart] as an application server.
|
||||
Before running the server for the first time, make sure to create the data folder:
|
||||
|
||||
```sh
|
||||
$ mkdir data
|
||||
```
|
||||
|
||||
To then run the server, use:
|
||||
|
||||
```sh
|
||||
$ pipenv run python -m lnbits
|
||||
|
|
|
@ -23,11 +23,11 @@ mkdir data
|
|||
./venv/bin/hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()'
|
||||
```
|
||||
|
||||
No you can visit your LNbits at http://localhost:5000/.
|
||||
Now you can visit your LNbits at http://localhost:5000/.
|
||||
|
||||
Now modify the `.env` file with any settings you prefer and add a proper [funding source](./wallets.md) by modifying the value of `LNBITS_BACKEND_WALLET_CLASS` and providing the extra information and credentials related to the chosen funding source.
|
||||
|
||||
Then you can run restart it and it will be using the new settings.
|
||||
Then you can restart it and it will be using the new settings.
|
||||
|
||||
You might also need to install additional packages or perform additional setup steps, depending on the chosen backend. See [the short guide](./wallets.md) on each different funding source.
|
||||
|
||||
|
@ -37,7 +37,7 @@ Docker installation
|
|||
To install using docker you first need to build the docker image as:
|
||||
```
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits/ # ${PWD} refered as <lnbits_repo>
|
||||
cd lnbits/ # ${PWD} referred as <lnbits_repo>
|
||||
docker build -t lnbits .
|
||||
```
|
||||
|
||||
|
@ -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.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import trio # type: ignore
|
||||
import trio
|
||||
|
||||
from .commands import migrate_databases, transpile_scss, bundle_vendored
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ from quart import g
|
|||
from quart_trio import QuartTrio
|
||||
from quart_cors import cors # type: ignore
|
||||
from quart_compress import Compress # type: ignore
|
||||
from secure import SecureHeaders # type: ignore
|
||||
|
||||
from .commands import db_migrate, handle_assets
|
||||
from .core import core_app
|
||||
|
@ -27,8 +26,6 @@ from .tasks import (
|
|||
)
|
||||
from .settings import WALLET
|
||||
|
||||
secure_headers = SecureHeaders(hsts=False, xfo=False)
|
||||
|
||||
|
||||
def create_app(config_object="lnbits.settings") -> QuartTrio:
|
||||
"""Create application factory.
|
||||
|
@ -46,7 +43,6 @@ def create_app(config_object="lnbits.settings") -> QuartTrio:
|
|||
register_blueprints(app)
|
||||
register_filters(app)
|
||||
register_commands(app)
|
||||
register_request_hooks(app)
|
||||
register_async_tasks(app)
|
||||
register_exception_handlers(app)
|
||||
|
||||
|
@ -108,19 +104,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():
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import trio # type: ignore
|
||||
import trio
|
||||
import json
|
||||
import httpx
|
||||
from io import BytesIO
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import trio # type: ignore
|
||||
import trio
|
||||
import httpx
|
||||
from typing import List
|
||||
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
></q-icon>
|
||||
{% raw %}
|
||||
<h5 class="q-mt-lg q-mb-xs">{{ extension.name }}</h5>
|
||||
{{ extension.shortDescription }} {% endraw %}
|
||||
<small>{{ extension.shortDescription }} </small>{% endraw %}
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-actions>
|
||||
<div v-if="extension.isEnabled">
|
||||
<q-btn
|
||||
flat
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
type="a"
|
||||
:href="[extension.url, '?usr=', g.user.id].join('')"
|
||||
>Open</q-btn
|
||||
|
@ -41,7 +41,7 @@
|
|||
<q-btn
|
||||
v-else
|
||||
flat
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
type="a"
|
||||
:href="['{{ url_for('core.extensions') }}', '?usr=', g.user.id, '&enable=', extension.code].join('')"
|
||||
>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{% if lnurl %}
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
@click="processing"
|
||||
type="a"
|
||||
href="{{ url_for('core.lnurlwallet', lightning=lnurl) }}"
|
||||
|
@ -25,7 +25,7 @@
|
|||
></q-input>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="walletName == ''"
|
||||
type="submit"
|
||||
>Add a new wallet</q-btn
|
||||
|
@ -37,58 +37,66 @@
|
|||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h3 class="q-my-none"><strong>LN</strong>bits</h3>
|
||||
<h5 class="q-my-md">Free and open-source lightning wallet</h5>
|
||||
<p>
|
||||
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!
|
||||
</p>
|
||||
<p>
|
||||
You can run LNbits for yourself, or easily offer a custodian solution
|
||||
for others.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<div class="row q-mt-md q-gutter-sm">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
type="a"
|
||||
href="https://github.com/lnbits/lnbits"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>View project in GitHub</q-btn
|
||||
>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
type="a"
|
||||
href="https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>Donate</q-btn
|
||||
>
|
||||
<h3 class="q-my-none">{{SITE_TITLE}}</h3>
|
||||
<h5 class="q-my-md">{{SITE_TAGLINE}}</h5>
|
||||
<div v-if="'{{SITE_TITLE}}' == 'LNbits'">
|
||||
<p>
|
||||
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!
|
||||
</p>
|
||||
<p>
|
||||
You can run LNbits for yourself, or easily offer a custodian
|
||||
solution for others.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<div class="row q-mt-md q-gutter-sm">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
type="a"
|
||||
href="https://github.com/lnbits/lnbits"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>View project in GitHub</q-btn
|
||||
>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
type="a"
|
||||
href="https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>Donate</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else>{{SITE_DESCRIPTION}}</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- Ads -->
|
||||
<div class="col-12 col-md-3 col-lg-3">
|
||||
<div class="col-12 col-md-3 col-lg-3" v-if="'{{SITE_TITLE}}' == 'LNbits'">
|
||||
<div class="row q-col-gutter-lg justify-center">
|
||||
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
|
||||
<q-btn flat color="purple" label="Runs on" class="full-width"></q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
color="secondary"
|
||||
label="Runs on"
|
||||
class="full-width"
|
||||
></q-btn>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="https://github.com/ElementsProject/lightning">
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<div class="col">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
class="full-width"
|
||||
@click="showParseDialog"
|
||||
>Paste Request</q-btn
|
||||
|
@ -31,7 +31,7 @@
|
|||
<div class="col">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
class="full-width"
|
||||
@click="showReceiveDialog"
|
||||
>Create Invoice</q-btn
|
||||
|
@ -40,7 +40,7 @@
|
|||
<div class="col">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="purple"
|
||||
color="secondary"
|
||||
icon="photo_camera"
|
||||
@click="showCamera"
|
||||
>scan
|
||||
|
@ -222,7 +222,7 @@
|
|||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
|
||||
LNbits wallet: <strong><em>{{ wallet.name }}</em></strong>
|
||||
{{ SITE_TITLE }} wallet: <strong><em>{{ wallet.name }}</em></strong>
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
|
@ -342,7 +342,7 @@
|
|||
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="receive.data.memo == null || receive.data.amount == null || receive.data.amount <= 0"
|
||||
type="submit"
|
||||
>
|
||||
|
@ -355,7 +355,7 @@
|
|||
</div>
|
||||
<q-spinner
|
||||
v-if="receive.status == 'loading'"
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
size="2.55em"
|
||||
></q-spinner>
|
||||
</q-form>
|
||||
|
@ -395,7 +395,7 @@
|
|||
</p>
|
||||
{% endraw %}
|
||||
<div v-if="canPay" class="row q-mt-lg">
|
||||
<q-btn unelevated color="deep-purple" @click="payInvoice">Pay</q-btn>
|
||||
<q-btn unelevated color="primary" @click="payInvoice">Pay</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
||||
</div>
|
||||
<div v-else class="row q-mt-lg">
|
||||
|
@ -423,7 +423,7 @@
|
|||
<code class="text-wrap"> {{ parse.lnurlauth.pubkey }} </code>
|
||||
</p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated color="deep-purple" type="submit">Login</q-btn>
|
||||
<q-btn unelevated color="primary" type="submit">Login</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
|
@ -485,9 +485,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated color="deep-purple" type="submit"
|
||||
>Send satoshis</q-btn
|
||||
>
|
||||
<q-btn unelevated color="primary" type="submit">Send satoshis</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
|
@ -512,7 +510,7 @@
|
|||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="parse.data.request == ''"
|
||||
type="submit"
|
||||
>Read</q-btn
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import trio # type: ignore
|
||||
import trio
|
||||
import json
|
||||
import lnurl # type: ignore
|
||||
import httpx
|
||||
|
|
|
@ -56,11 +56,11 @@ async def extensions():
|
|||
|
||||
if extension_to_enable:
|
||||
await update_user_extension(
|
||||
user_id=g.user.id, extension=extension_to_enable, active=1
|
||||
user_id=g.user.id, extension=extension_to_enable, active=True
|
||||
)
|
||||
elif extension_to_disable:
|
||||
await update_user_extension(
|
||||
user_id=g.user.id, extension=extension_to_disable, active=0
|
||||
user_id=g.user.id, extension=extension_to_disable, active=False
|
||||
)
|
||||
|
||||
return await render_template("core/extensions.html", user=await get_user(g.user.id))
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import trio # type: ignore
|
||||
import trio
|
||||
import datetime
|
||||
from http import HTTPStatus
|
||||
from quart import jsonify
|
||||
|
@ -32,6 +32,24 @@ async def api_public_payment_longpolling(payment_hash):
|
|||
print("adding standalone invoice listener", payment_hash, send_payment)
|
||||
api_invoice_listeners.append(send_payment)
|
||||
|
||||
async for payment in receive_payment:
|
||||
if payment.payment_hash == payment_hash:
|
||||
return jsonify({"status": "paid"}), HTTPStatus.OK
|
||||
response = None
|
||||
|
||||
async def payment_info_receiver(cancel_scope):
|
||||
async for payment in receive_payment:
|
||||
if payment.payment_hash == payment_hash:
|
||||
nonlocal response
|
||||
response = (jsonify({"status": "paid"}), HTTPStatus.OK)
|
||||
cancel_scope.cancel()
|
||||
|
||||
async def timeouter(cancel_scope):
|
||||
await trio.sleep(45)
|
||||
cancel_scope.cancel()
|
||||
|
||||
async with trio.open_nursery() as nursery:
|
||||
nursery.start_soon(payment_info_receiver, nursery.cancel_scope)
|
||||
nursery.start_soon(timeouter, nursery.cancel_scope)
|
||||
|
||||
if response:
|
||||
return response
|
||||
else:
|
||||
return jsonify({"message": "timeout"}), HTTPStatus.REQUEST_TIMEOUT
|
||||
|
|
2
lnbits/data/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
127
lnbits/db.py
|
@ -1,36 +1,125 @@
|
|||
import os
|
||||
import trio
|
||||
import time
|
||||
from typing import Optional
|
||||
from contextlib import asynccontextmanager
|
||||
from sqlalchemy import create_engine # type: ignore
|
||||
from sqlalchemy_aio import TRIO_STRATEGY # type: ignore
|
||||
from sqlalchemy_aio.base import AsyncConnection
|
||||
from sqlalchemy_aio.base import AsyncConnection # type: ignore
|
||||
|
||||
from .settings import LNBITS_DATA_FOLDER
|
||||
from .settings import LNBITS_DATA_FOLDER, LNBITS_DATABASE_URL
|
||||
|
||||
POSTGRES = "POSTGRES"
|
||||
COCKROACH = "COCKROACH"
|
||||
SQLITE = "SQLITE"
|
||||
|
||||
|
||||
class Connection:
|
||||
def __init__(self, conn: AsyncConnection):
|
||||
class Compat:
|
||||
type: Optional[str] = "<inherited>"
|
||||
schema: Optional[str] = "<inherited>"
|
||||
|
||||
def interval_seconds(self, seconds: int) -> str:
|
||||
if self.type in {POSTGRES, COCKROACH}:
|
||||
return f"interval '{seconds} seconds'"
|
||||
elif self.type == SQLITE:
|
||||
return f"{seconds}"
|
||||
return "<nothing>"
|
||||
|
||||
@property
|
||||
def timestamp_now(self) -> str:
|
||||
if self.type in {POSTGRES, COCKROACH}:
|
||||
return "now()"
|
||||
elif self.type == SQLITE:
|
||||
return "(strftime('%s', 'now'))"
|
||||
return "<nothing>"
|
||||
|
||||
@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 "<nothing>"
|
||||
|
||||
@property
|
||||
def references_schema(self) -> str:
|
||||
if self.type in {POSTGRES, COCKROACH}:
|
||||
return f"{self.schema}."
|
||||
elif self.type == SQLITE:
|
||||
return ""
|
||||
return "<nothing>"
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
|
|
@ -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,))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="amilkDialog.show = true"
|
||||
<q-btn unelevated color="primary" @click="amilkDialog.show = true"
|
||||
>New AMilk</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
|
@ -109,7 +109,7 @@
|
|||
></q-input>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="amilkDialog.data.amount == null || amilkDialog.data.amount < 0 || amilkDialog.data.lnurl == null"
|
||||
type="submit"
|
||||
>Create amilk</q-btn
|
||||
|
|
|
@ -21,7 +21,7 @@ async def create_bleskomat(
|
|||
api_key_encoding = "hex"
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO bleskomats (id, wallet, api_key_id, api_key_secret, api_key_encoding, name, fiat_currency, exchange_rate_provider, fee)
|
||||
INSERT INTO bleskomat.bleskomats (id, wallet, api_key_id, api_key_secret, api_key_encoding, name, fiat_currency, exchange_rate_provider, fee)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
|
@ -42,13 +42,15 @@ async def create_bleskomat(
|
|||
|
||||
|
||||
async def get_bleskomat(bleskomat_id: str) -> 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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
|
||||
<q-btn unelevated color="primary" @click="formDialog.show = true"
|
||||
>Add Bleskomat</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
|
@ -150,14 +150,14 @@
|
|||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Update Bleskomat</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="
|
||||
formDialog.data.wallet == null ||
|
||||
formDialog.data.name == null ||
|
||||
|
|
|
@ -18,7 +18,7 @@ async def create_captcha(
|
|||
captcha_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO captchas (id, wallet, url, memo, description, amount, remembers)
|
||||
INSERT INTO captcha.captchas (id, wallet, url, memo, description, amount, remembers)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(captcha_id, wallet_id, url, memo, description, amount, int(remembers)),
|
||||
|
@ -30,7 +30,9 @@ async def create_captcha(
|
|||
|
||||
|
||||
async def get_captcha(captcha_id: str) -> 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,))
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
dense
|
||||
flat
|
||||
icon="check"
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
type="submit"
|
||||
@click="createInvoice"
|
||||
:disabled="userAmount < captchaAmount || paymentReq"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
|
||||
<q-btn unelevated color="primary" @click="formDialog.show = true"
|
||||
>New captcha</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
|
@ -141,7 +141,7 @@
|
|||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
v-model="formDialog.data.remembers"
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
></q-checkbox>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
|
@ -157,7 +157,7 @@
|
|||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="formDialog.data.amount == null || formDialog.data.amount < 0 || formDialog.data.memo == null"
|
||||
type="submit"
|
||||
>Create captcha</q-btn
|
||||
|
|
3
lnbits/extensions/copilot/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# StreamerCopilot
|
||||
|
||||
Tool to help streamers accept sats for tips
|
17
lnbits/extensions/copilot/__init__.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from quart import Blueprint
|
||||
from lnbits.db import Database
|
||||
|
||||
db = Database("ext_copilot")
|
||||
|
||||
copilot_ext: Blueprint = Blueprint(
|
||||
"copilot", __name__, static_folder="static", template_folder="templates"
|
||||
)
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
from .lnurl import * # noqa
|
||||
from .tasks import register_listeners
|
||||
|
||||
from lnbits.tasks import record_async
|
||||
|
||||
copilot_ext.record(record_async(register_listeners))
|
8
lnbits/extensions/copilot/config.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "StreamerCopilot",
|
||||
"short_description": "Video tips/animations/webhooks",
|
||||
"icon": "face",
|
||||
"contributors": [
|
||||
"arcbtc"
|
||||
]
|
||||
}
|
109
lnbits/extensions/copilot/crud.py
Normal file
|
@ -0,0 +1,109 @@
|
|||
from typing import List, Optional, Union
|
||||
|
||||
# from lnbits.db import open_ext_db
|
||||
from . import db
|
||||
from .models import Copilots
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from quart import jsonify
|
||||
|
||||
|
||||
###############COPILOTS##########################
|
||||
|
||||
|
||||
async def create_copilot(
|
||||
title: str,
|
||||
user: str,
|
||||
lnurl_toggle: Optional[int] = 0,
|
||||
wallet: Optional[str] = None,
|
||||
animation1: Optional[str] = None,
|
||||
animation2: Optional[str] = None,
|
||||
animation3: Optional[str] = None,
|
||||
animation1threshold: Optional[int] = None,
|
||||
animation2threshold: Optional[int] = None,
|
||||
animation3threshold: Optional[int] = None,
|
||||
animation1webhook: Optional[str] = None,
|
||||
animation2webhook: Optional[str] = None,
|
||||
animation3webhook: Optional[str] = None,
|
||||
lnurl_title: Optional[str] = None,
|
||||
show_message: Optional[int] = 0,
|
||||
show_ack: Optional[int] = 0,
|
||||
show_price: Optional[int] = 0,
|
||||
amount_made: Optional[int] = None,
|
||||
) -> Copilots:
|
||||
copilot_id = urlsafe_short_hash()
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO copilots (
|
||||
id,
|
||||
"user",
|
||||
lnurl_toggle,
|
||||
wallet,
|
||||
title,
|
||||
animation1,
|
||||
animation2,
|
||||
animation3,
|
||||
animation1threshold,
|
||||
animation2threshold,
|
||||
animation3threshold,
|
||||
animation1webhook,
|
||||
animation2webhook,
|
||||
animation3webhook,
|
||||
lnurl_title,
|
||||
show_message,
|
||||
show_ack,
|
||||
show_price,
|
||||
lnurl_title,
|
||||
amount_made
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
copilot_id,
|
||||
user,
|
||||
lnurl_toggle,
|
||||
wallet,
|
||||
title,
|
||||
animation1,
|
||||
animation2,
|
||||
animation3,
|
||||
animation1threshold,
|
||||
animation2threshold,
|
||||
animation3threshold,
|
||||
animation1webhook,
|
||||
animation2webhook,
|
||||
animation3webhook,
|
||||
lnurl_title,
|
||||
show_message,
|
||||
show_ack,
|
||||
show_price,
|
||||
lnurl_title,
|
||||
0,
|
||||
),
|
||||
)
|
||||
return await get_copilot(copilot_id)
|
||||
|
||||
|
||||
async def update_copilot(copilot_id: str, **kwargs) -> Optional[Copilots]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE copilots SET {q} WHERE id = ?", (*kwargs.values(), copilot_id)
|
||||
)
|
||||
row = await db.fetchone("SELECT * FROM copilots WHERE id = ?", (copilot_id,))
|
||||
return Copilots.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_copilot(copilot_id: str) -> Copilots:
|
||||
row = await db.fetchone("SELECT * FROM copilots WHERE id = ?", (copilot_id,))
|
||||
return Copilots.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_copilots(user: str) -> List[Copilots]:
|
||||
rows = await db.fetchall("""SELECT * FROM copilots WHERE "user" = ?""", (user,))
|
||||
return [Copilots.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def delete_copilot(copilot_id: str) -> None:
|
||||
await db.execute("DELETE FROM copilots WHERE id = ?", (copilot_id,))
|
86
lnbits/extensions/copilot/lnurl.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
import json
|
||||
import hashlib
|
||||
import math
|
||||
from quart import jsonify, url_for, request
|
||||
from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore
|
||||
from lnurl.types import LnurlPayMetadata
|
||||
from lnbits.core.services import create_invoice
|
||||
|
||||
from . import copilot_ext
|
||||
from .crud import get_copilot
|
||||
|
||||
|
||||
@copilot_ext.route("/lnurl/<cp_id>", methods=["GET"])
|
||||
async def lnurl_response(cp_id):
|
||||
cp = await get_copilot(cp_id)
|
||||
if not cp:
|
||||
return jsonify({"status": "ERROR", "reason": "Copilot not found."})
|
||||
|
||||
resp = LnurlPayResponse(
|
||||
callback=url_for("copilot.lnurl_callback", cp_id=cp_id, _external=True),
|
||||
min_sendable=10000,
|
||||
max_sendable=50000000,
|
||||
metadata=LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]])),
|
||||
)
|
||||
|
||||
params = resp.dict()
|
||||
if cp.show_message:
|
||||
params["commentAllowed"] = 300
|
||||
|
||||
return jsonify(params)
|
||||
|
||||
|
||||
@copilot_ext.route("/lnurl/cb/<cp_id>", methods=["GET"])
|
||||
async def lnurl_callback(cp_id):
|
||||
cp = await get_copilot(cp_id)
|
||||
if not cp:
|
||||
return jsonify({"status": "ERROR", "reason": "Copilot not found."})
|
||||
|
||||
amount_received = int(request.args.get("amount"))
|
||||
|
||||
if amount_received < 10000:
|
||||
return (
|
||||
jsonify(
|
||||
LnurlErrorResponse(
|
||||
reason=f"Amount {round(amount_received / 1000)} is smaller than minimum 10 sats."
|
||||
).dict()
|
||||
),
|
||||
)
|
||||
elif amount_received / 1000 > 10000000:
|
||||
return (
|
||||
jsonify(
|
||||
LnurlErrorResponse(
|
||||
reason=f"Amount {round(amount_received / 1000)} is greater than maximum 50000."
|
||||
).dict()
|
||||
),
|
||||
)
|
||||
comment = ""
|
||||
if request.args.get("comment"):
|
||||
comment = request.args.get("comment")
|
||||
if len(comment or "") > 300:
|
||||
return jsonify(
|
||||
LnurlErrorResponse(
|
||||
reason=f"Got a comment with {len(comment)} characters, but can only accept 300"
|
||||
).dict()
|
||||
)
|
||||
if len(comment) < 1:
|
||||
comment = "none"
|
||||
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=cp.wallet,
|
||||
amount=int(amount_received / 1000),
|
||||
memo=cp.lnurl_title,
|
||||
description_hash=hashlib.sha256(
|
||||
(
|
||||
LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]]))
|
||||
).encode("utf-8")
|
||||
).digest(),
|
||||
extra={"tag": "copilot", "copilot": cp.id, "comment": comment},
|
||||
)
|
||||
resp = LnurlPayActionResponse(
|
||||
pr=payment_request,
|
||||
success_action=None,
|
||||
disposable=False,
|
||||
routes=[],
|
||||
)
|
||||
return jsonify(resp.dict())
|
33
lnbits/extensions/copilot/migrations.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
async def m001_initial(db):
|
||||
"""
|
||||
Initial copilot table.
|
||||
"""
|
||||
|
||||
await db.execute(
|
||||
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}
|
||||
);
|
||||
"""
|
||||
)
|
41
lnbits/extensions/copilot/models.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
from sqlite3 import Row
|
||||
from typing import NamedTuple
|
||||
import time
|
||||
from quart import url_for
|
||||
from lnurl import Lnurl, encode as lnurl_encode # type: ignore
|
||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
|
||||
|
||||
|
||||
class Copilots(NamedTuple):
|
||||
id: str
|
||||
user: str
|
||||
title: str
|
||||
lnurl_toggle: int
|
||||
wallet: str
|
||||
animation1: str
|
||||
animation2: str
|
||||
animation3: str
|
||||
animation1threshold: int
|
||||
animation2threshold: int
|
||||
animation3threshold: int
|
||||
animation1webhook: str
|
||||
animation2webhook: str
|
||||
animation3webhook: str
|
||||
lnurl_title: str
|
||||
show_message: int
|
||||
show_ack: int
|
||||
show_price: int
|
||||
amount_made: int
|
||||
timestamp: int
|
||||
fullscreen_cam: int
|
||||
iframe_url: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Copilots":
|
||||
return cls(**dict(row))
|
||||
|
||||
@property
|
||||
def lnurl(self) -> Lnurl:
|
||||
url = url_for("copilot.lnurl_response", cp_id=self.id, _external=True)
|
||||
return lnurl_encode(url)
|
BIN
lnbits/extensions/copilot/static/bitcoin.gif
Normal file
After Width: | Height: | Size: 308 KiB |
BIN
lnbits/extensions/copilot/static/confetti.gif
Normal file
After Width: | Height: | Size: 333 KiB |
BIN
lnbits/extensions/copilot/static/face.gif
Normal file
After Width: | Height: | Size: 536 KiB |
BIN
lnbits/extensions/copilot/static/lnurl.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
lnbits/extensions/copilot/static/martijn.gif
Normal file
After Width: | Height: | Size: 504 KiB |
BIN
lnbits/extensions/copilot/static/rick.gif
Normal file
After Width: | Height: | Size: 2.3 MiB |
BIN
lnbits/extensions/copilot/static/rocket.gif
Normal file
After Width: | Height: | Size: 577 KiB |
88
lnbits/extensions/copilot/tasks.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
import trio # type: ignore
|
||||
import json
|
||||
import httpx
|
||||
from quart import g, jsonify, url_for, websocket
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_copilot
|
||||
from .views import updater
|
||||
import shortuuid
|
||||
|
||||
|
||||
async def register_listeners():
|
||||
invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2)
|
||||
register_invoice_listener(invoice_paid_chan_send)
|
||||
await wait_for_paid_invoices(invoice_paid_chan_recv)
|
||||
|
||||
|
||||
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
||||
async for payment in invoice_paid_chan:
|
||||
await on_invoice_paid(payment)
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
webhook = None
|
||||
data = None
|
||||
if "copilot" != payment.extra.get("tag"):
|
||||
# not an copilot invoice
|
||||
return
|
||||
|
||||
if payment.extra.get("wh_status"):
|
||||
# this webhook has already been sent
|
||||
return
|
||||
|
||||
copilot = await get_copilot(payment.extra.get("copilot", -1))
|
||||
|
||||
if not copilot:
|
||||
return (
|
||||
jsonify({"message": "Copilot link link does not exist."}),
|
||||
HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
if copilot.animation1threshold:
|
||||
if int(payment.amount / 1000) >= copilot.animation1threshold:
|
||||
data = copilot.animation1
|
||||
webhook = copilot.animation1webhook
|
||||
if copilot.animation2threshold:
|
||||
if int(payment.amount / 1000) >= copilot.animation2threshold:
|
||||
data = copilot.animation2
|
||||
webhook = copilot.animation1webhook
|
||||
if copilot.animation3threshold:
|
||||
if int(payment.amount / 1000) >= copilot.animation3threshold:
|
||||
data = copilot.animation3
|
||||
webhook = copilot.animation1webhook
|
||||
if webhook:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
r = await client.post(
|
||||
webhook,
|
||||
json={
|
||||
"payment_hash": payment.payment_hash,
|
||||
"payment_request": payment.bolt11,
|
||||
"amount": payment.amount,
|
||||
"comment": payment.extra.get("comment"),
|
||||
},
|
||||
timeout=40,
|
||||
)
|
||||
await mark_webhook_sent(payment, r.status_code)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
await mark_webhook_sent(payment, -1)
|
||||
if payment.extra.get("comment"):
|
||||
await updater(copilot.id, data, payment.extra.get("comment"))
|
||||
else:
|
||||
await updater(copilot.id, data, "none")
|
||||
|
||||
|
||||
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||
payment.extra["wh_status"] = status
|
||||
|
||||
await core_db.execute(
|
||||
"""
|
||||
UPDATE apipayments SET extra = ?
|
||||
WHERE hash = ?
|
||||
""",
|
||||
(json.dumps(payment.extra), payment.payment_hash),
|
||||
)
|
172
lnbits/extensions/copilot/templates/copilot/_api_docs.html
Normal file
|
@ -0,0 +1,172 @@
|
|||
<q-card>
|
||||
<q-card-section>
|
||||
<p>
|
||||
StreamerCopilot: get tips via static QR (lnurl-pay) and show an
|
||||
animation<br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" dense expand-separator label="Create copilot">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">POST</span> /copilot/api/v1/copilot</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<copilot_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}api/v1/copilot -d '{"title":
|
||||
<string>, "animation": <string>,
|
||||
"show_message":<string>, "amount": <integer>,
|
||||
"lnurl_title": <string>}' -H "Content-type: application/json"
|
||||
-H "X-Api-Key: {{g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Update copilot">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">PUT</span>
|
||||
/copilot/api/v1/copilot/<copilot_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<copilot_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root
|
||||
}}api/v1/copilot/<copilot_id> -d '{"title": <string>,
|
||||
"animation": <string>, "show_message":<string>,
|
||||
"amount": <integer>, "lnurl_title": <string>}' -H
|
||||
"Content-type: application/json" -H "X-Api-Key:
|
||||
{{g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item group="api" dense expand-separator label="Get copilot">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/copilot/api/v1/copilot/<copilot_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<copilot_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/copilot/<copilot_id>
|
||||
-H "X-Api-Key: {{ g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Get copilots">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span> /copilot/api/v1/copilots</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<copilot_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/copilots -H "X-Api-Key: {{
|
||||
g.user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Delete a pay link"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-pink">DELETE</span>
|
||||
/copilot/api/v1/copilot/<copilot_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
|
||||
<code></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root
|
||||
}}api/v1/copilot/<copilot_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Trigger an animation"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/api/v1/copilot/ws/<copilot_id>/<comment>/<data></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 200</h5>
|
||||
<code></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}/api/v1/copilot/ws/<string,
|
||||
copilot_id>/<string, comment>/<string, gif name> -H
|
||||
"X-Api-Key: {{ g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
||||
</q-card>
|
289
lnbits/extensions/copilot/templates/copilot/compose.html
Normal file
|
@ -0,0 +1,289 @@
|
|||
{% extends "public.html" %} {% block page %}<q-page>
|
||||
<video
|
||||
autoplay="true"
|
||||
id="videoScreen"
|
||||
style="width: 100%"
|
||||
class="fixed-bottom-right"
|
||||
></video>
|
||||
<video
|
||||
autoplay="true"
|
||||
id="videoCamera"
|
||||
style="width: 100%"
|
||||
class="fixed-bottom-right"
|
||||
></video>
|
||||
<img src="" style="width: 100%" id="animations" class="fixed-bottom-left" />
|
||||
|
||||
<div
|
||||
v-if="copilot.lnurl_toggle == 1"
|
||||
class="rounded-borders column fixed-right"
|
||||
style="
|
||||
width: 250px;
|
||||
background-color: white;
|
||||
height: 300px;
|
||||
margin-top: 10%;
|
||||
"
|
||||
>
|
||||
<div class="col">
|
||||
<qrcode
|
||||
:value="copilot.lnurl"
|
||||
:options="{width:250}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
<center class="absolute-bottom" style="color: black; font-size: 20px">
|
||||
{% raw %}{{ copilot.lnurl_title }}{% endraw %}
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
v-if="copilot.show_price != 0"
|
||||
class="text-bold fixed-bottom-left"
|
||||
style="
|
||||
margin: 60px 60px;
|
||||
font-size: 110px;
|
||||
text-shadow: 4px 8px 4px black;
|
||||
color: white;
|
||||
"
|
||||
>
|
||||
{% raw %}{{ price }}{% endraw %}
|
||||
</h2>
|
||||
<p
|
||||
v-if="copilot.show_ack != 0"
|
||||
class="fixed-top"
|
||||
style="
|
||||
font-size: 22px;
|
||||
text-shadow: 2px 4px 1px black;
|
||||
color: white;
|
||||
padding-left: 40%;
|
||||
"
|
||||
>
|
||||
Powered by LNbits/StreamerCopilot
|
||||
</p>
|
||||
</q-page>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<style>
|
||||
body.body--dark .q-drawer,
|
||||
body.body--dark .q-footer,
|
||||
body.body--dark .q-header,
|
||||
.q-drawer,
|
||||
.q-footer,
|
||||
.q-header {
|
||||
display: none;
|
||||
}
|
||||
.q-page {
|
||||
padding: 0px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data() {
|
||||
return {
|
||||
price: '',
|
||||
counter: 1,
|
||||
colours: ['teal', 'purple', 'indigo', 'pink', 'green'],
|
||||
copilot: {},
|
||||
animQueue: [],
|
||||
queue: false,
|
||||
lnurl: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showNotif: function (userMessage) {
|
||||
var colour = this.colours[
|
||||
Math.floor(Math.random() * this.colours.length)
|
||||
]
|
||||
this.$q.notify({
|
||||
color: colour,
|
||||
icon: 'chat_bubble_outline',
|
||||
html: true,
|
||||
message: '<h4 style="color: white;">' + userMessage + '</h4>',
|
||||
position: 'top-left',
|
||||
timeout: 5000
|
||||
})
|
||||
},
|
||||
openURL: function (url) {
|
||||
return Quasar.utils.openURL(url)
|
||||
},
|
||||
initCamera() {
|
||||
var video = document.querySelector('#videoCamera')
|
||||
|
||||
if (navigator.mediaDevices.getUserMedia) {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({video: true})
|
||||
.then(function (stream) {
|
||||
video.srcObject = stream
|
||||
})
|
||||
.catch(function (err0r) {
|
||||
console.log('Something went wrong!')
|
||||
})
|
||||
}
|
||||
},
|
||||
initScreenShare() {
|
||||
var video = document.querySelector('#videoScreen')
|
||||
navigator.mediaDevices
|
||||
.getDisplayMedia({video: true})
|
||||
.then(function (stream) {
|
||||
video.srcObject = stream
|
||||
})
|
||||
.catch(function (err0r) {
|
||||
console.log('Something went wrong!')
|
||||
})
|
||||
},
|
||||
pushAnim(content) {
|
||||
document.getElementById('animations').style.width = content[0]
|
||||
document.getElementById('animations').src = content[1]
|
||||
if (content[2] != 'none') {
|
||||
self.showNotif(content[2])
|
||||
}
|
||||
setTimeout(function () {
|
||||
document.getElementById('animations').src = ''
|
||||
}, 5000)
|
||||
},
|
||||
launch() {
|
||||
self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/copilot/api/v1/copilot/ws/' +
|
||||
self.copilot.id +
|
||||
'/launching/rocket'
|
||||
)
|
||||
.then(function (response1) {
|
||||
self.$q.notify({
|
||||
color: 'green',
|
||||
message: 'Sent!'
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initCamera()
|
||||
},
|
||||
created: function () {
|
||||
self = this
|
||||
self.copilot = JSON.parse(localStorage.getItem('copilot'))
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/copilot/api/v1/copilot/' + self.copilot.id,
|
||||
localStorage.getItem('inkey')
|
||||
)
|
||||
.then(function (response) {
|
||||
self.copilot = response.data
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
|
||||
this.connectionBitStamp = new WebSocket('wss://ws.bitstamp.net')
|
||||
|
||||
const obj = JSON.stringify({
|
||||
event: 'bts:subscribe',
|
||||
data: {channel: 'live_trades_' + self.copilot.show_price}
|
||||
})
|
||||
|
||||
this.connectionBitStamp.onmessage = function (e) {
|
||||
if (self.copilot.show_price) {
|
||||
if (self.copilot.show_price == 'btcusd') {
|
||||
self.price = String(
|
||||
new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(JSON.parse(e.data).data.price)
|
||||
)
|
||||
} else if (self.copilot.show_price == 'btceur') {
|
||||
self.price = String(
|
||||
new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(JSON.parse(e.data).data.price)
|
||||
)
|
||||
} else if (self.copilot.show_price == 'btcgbp') {
|
||||
self.price = String(
|
||||
new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'GBP'
|
||||
}).format(JSON.parse(e.data).data.price)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.connectionBitStamp.onopen = () => this.connectionBitStamp.send(obj)
|
||||
|
||||
const fetch = data =>
|
||||
new Promise(resolve => setTimeout(resolve, 5000, this.pushAnim(data)))
|
||||
|
||||
const addTask = (() => {
|
||||
let pending = Promise.resolve()
|
||||
const run = async data => {
|
||||
try {
|
||||
await pending
|
||||
} finally {
|
||||
return fetch(data)
|
||||
}
|
||||
}
|
||||
return data => (pending = run(data))
|
||||
})()
|
||||
|
||||
if (location.protocol !== 'http:') {
|
||||
localUrl =
|
||||
'wss://' +
|
||||
document.domain +
|
||||
':' +
|
||||
location.port +
|
||||
'/copilot/ws/' +
|
||||
self.copilot.id +
|
||||
'/'
|
||||
} else {
|
||||
localUrl =
|
||||
'ws://' +
|
||||
document.domain +
|
||||
':' +
|
||||
location.port +
|
||||
'/copilot/ws/' +
|
||||
self.copilot.id +
|
||||
'/'
|
||||
}
|
||||
this.connection = new WebSocket(localUrl)
|
||||
this.connection.onmessage = function (e) {
|
||||
res = e.data.split('-')
|
||||
if (res[0] == 'rocket') {
|
||||
addTask(['40%', '/copilot/static/rocket.gif', res[1]])
|
||||
}
|
||||
if (res[0] == 'face') {
|
||||
addTask(['35%', '/copilot/static/face.gif', res[1]])
|
||||
}
|
||||
if (res[0] == 'bitcoin') {
|
||||
addTask(['30%', '/copilot/static/bitcoin.gif', res[1]])
|
||||
}
|
||||
if (res[0] == 'confetti') {
|
||||
addTask(['100%', '/copilot/static/confetti.gif', res[1]])
|
||||
}
|
||||
if (res[0] == 'martijn') {
|
||||
addTask(['40%', '/copilot/static/martijn.gif', res[1]])
|
||||
}
|
||||
if (res[0] == 'rick') {
|
||||
addTask(['40%', '/copilot/static/rick.gif', res[1]])
|
||||
}
|
||||
if (res[0] == 'true') {
|
||||
document.getElementById('videoCamera').style.width = '20%'
|
||||
self.initScreenShare()
|
||||
}
|
||||
if (res[0] == 'false') {
|
||||
document.getElementById('videoCamera').style.width = '100%'
|
||||
document.getElementById('videoScreen').src = null
|
||||
}
|
||||
}
|
||||
this.connection.onopen = () => this.launch
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
637
lnbits/extensions/copilot/templates/copilot/index.html
Normal file
|
@ -0,0 +1,637 @@
|
|||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
{% raw %}
|
||||
<q-btn unelevated color="primary" @click="formDialogCopilot.show = true"
|
||||
>New copilot instance
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Copilots</h5>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<q-input
|
||||
borderless
|
||||
dense
|
||||
debounce="300"
|
||||
v-model="filter"
|
||||
placeholder="Search"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon name="search"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-btn flat color="grey" @click="exportcopilotCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
flat
|
||||
dense
|
||||
:data="CopilotLinks"
|
||||
row-key="id"
|
||||
:columns="CopilotsTable.columns"
|
||||
:pagination.sync="CopilotsTable.pagination"
|
||||
:filter="filter"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th style="width: 5%"></q-th>
|
||||
<q-th style="width: 5%"></q-th>
|
||||
<q-th style="width: 5%"></q-th>
|
||||
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
auto-width
|
||||
>
|
||||
<div v-if="col.name == 'id'"></div>
|
||||
<div v-else>{{ col.label }}</div>
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="apps"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openCopilotPanel(props.row.id)"
|
||||
>
|
||||
<q-tooltip> Panel </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="face"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openCopilotCompose(props.row.id)"
|
||||
>
|
||||
<q-tooltip> Compose window </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteCopilotLink(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
>
|
||||
<q-tooltip> Delete copilot </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="openUpdateCopilotLink(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
>
|
||||
<q-tooltip> Edit copilot </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
auto-width
|
||||
>
|
||||
<div v-if="col.name == 'id'"></div>
|
||||
<div v-else>{{ col.value }}</div>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits StreamCopilot Extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> {% include "copilot/_api_docs.html" %} </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<q-dialog
|
||||
v-model="formDialogCopilot.show"
|
||||
position="top"
|
||||
@hide="closeFormDialog"
|
||||
>
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendFormDataCopilot" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCopilot.data.title"
|
||||
type="text"
|
||||
label="Title"
|
||||
></q-input>
|
||||
<div class="row">
|
||||
<q-checkbox
|
||||
v-model="formDialogCopilot.data.lnurl_toggle"
|
||||
label="Include lnurl payment QR? (requires https)"
|
||||
left-label
|
||||
></q-checkbox>
|
||||
</div>
|
||||
|
||||
<div v-if="formDialogCopilot.data.lnurl_toggle">
|
||||
<q-checkbox
|
||||
v-model="formDialogCopilot.data.show_message"
|
||||
left-label
|
||||
label="Show lnurl-pay messages? (supported by few wallets)"
|
||||
></q-checkbox>
|
||||
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialogCopilot.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
></q-select>
|
||||
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Payment threshold 1"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCopilot.data.animation1"
|
||||
:options="options"
|
||||
label="Animation"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col q-pl-xs">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCopilot.data.animation1threshold"
|
||||
type="number"
|
||||
label="From *sats"
|
||||
:min="10"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col q-pl-xs">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCopilot.data.animation1webhook"
|
||||
type="text"
|
||||
label="Webhook"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Payment threshold 2 (Must be higher than last)"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div
|
||||
class="row"
|
||||
v-if="formDialogCopilot.data.animation1threshold > 0"
|
||||
>
|
||||
<div class="col">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCopilot.data.animation2"
|
||||
:options="options"
|
||||
label="Animation"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col q-pl-xs">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="formDialogCopilot.data.animation2threshold"
|
||||
type="number"
|
||||
label="From *sats"
|
||||
:min="formDialogCopilot.data.animation1threshold"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col q-pl-xs">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCopilot.data.animation2webhook"
|
||||
type="text"
|
||||
label="Webhook"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Payment threshold 3 (Must be higher than last)"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div
|
||||
class="row"
|
||||
v-if="formDialogCopilot.data.animation2threshold > formDialogCopilot.data.animation1threshold"
|
||||
>
|
||||
<div class="col">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCopilot.data.animation3"
|
||||
:options="options"
|
||||
label="Animation"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col q-pl-xs">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="formDialogCopilot.data.animation3threshold"
|
||||
type="number"
|
||||
label="From *sats"
|
||||
:min="formDialogCopilot.data.animation2threshold"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col q-pl-xs">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCopilot.data.animation3webhook"
|
||||
type="text"
|
||||
label="Webhook"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCopilot.data.lnurl_title"
|
||||
type="text"
|
||||
max="1440"
|
||||
label="Lnurl title (message with QR code)"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="q-gutter-sm">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
style="width: 50%"
|
||||
v-model.trim="formDialogCopilot.data.show_price"
|
||||
:options="currencyOptions"
|
||||
label="Show price"
|
||||
/>
|
||||
</div>
|
||||
<div class="q-gutter-sm">
|
||||
<div class="row">
|
||||
<q-checkbox
|
||||
v-model="formDialogCopilot.data.show_ack"
|
||||
left-label
|
||||
label="Show 'powered by LNbits'"
|
||||
></q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialogCopilot.data.id"
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="
|
||||
formDialogCopilot.data.title == ''"
|
||||
type="submit"
|
||||
>Update Copilot</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="
|
||||
formDialogCopilot.data.title == ''"
|
||||
type="submit"
|
||||
>Create Copilot</q-btn
|
||||
>
|
||||
<q-btn @click="cancelCopilot" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<style></style>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
var mapCopilot = obj => {
|
||||
obj._data = _.clone(obj)
|
||||
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
|
||||
obj.time = obj.time + 'mins'
|
||||
|
||||
if (obj.time_elapsed) {
|
||||
obj.date = 'Time elapsed'
|
||||
} else {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date((obj.theTime - 3600) * 1000),
|
||||
'HH:mm:ss'
|
||||
)
|
||||
}
|
||||
obj.displayComposeUrl = ['/copilot/cp/', obj.id].join('')
|
||||
obj.displayPanelUrl = ['/copilot/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
filter: '',
|
||||
CopilotLinks: [],
|
||||
CopilotLinksObj: [],
|
||||
CopilotsTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'theId',
|
||||
align: 'left',
|
||||
label: 'id',
|
||||
field: 'id'
|
||||
},
|
||||
{
|
||||
name: 'lnurl_toggle',
|
||||
align: 'left',
|
||||
label: 'Show lnurl pay link',
|
||||
field: 'lnurl_toggle'
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
align: 'left',
|
||||
label: 'title',
|
||||
field: 'title'
|
||||
},
|
||||
{
|
||||
name: 'amount_made',
|
||||
align: 'left',
|
||||
label: 'amount made',
|
||||
field: 'amount_made'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
passedCopilot: {},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
formDialogCopilot: {
|
||||
show: false,
|
||||
data: {
|
||||
lnurl_toggle: false,
|
||||
show_message: false,
|
||||
show_ack: false,
|
||||
show_price: 'None',
|
||||
title: ''
|
||||
}
|
||||
},
|
||||
qrCodeDialog: {
|
||||
show: false,
|
||||
data: null
|
||||
},
|
||||
options: ['bitcoin', 'confetti', 'rocket', 'face', 'martijn', 'rick'],
|
||||
currencyOptions: ['None', 'btcusd', 'btceur', 'btcgbp']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelCopilot: function (data) {
|
||||
var self = this
|
||||
self.formDialogCopilot.show = false
|
||||
},
|
||||
closeFormDialog: function () {
|
||||
this.formDialog.data = {
|
||||
is_unique: false
|
||||
}
|
||||
},
|
||||
sendFormDataCopilot: function () {
|
||||
var self = this
|
||||
if (self.formDialogCopilot.data.id) {
|
||||
this.updateCopilot(
|
||||
self.g.user.wallets[0].adminkey,
|
||||
self.formDialogCopilot.data
|
||||
)
|
||||
} else {
|
||||
this.createCopilot(
|
||||
self.g.user.wallets[0].adminkey,
|
||||
self.formDialogCopilot.data
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
createCopilot: function (wallet, data) {
|
||||
var self = this
|
||||
var updatedData = {}
|
||||
for (const property in data) {
|
||||
if (data[property]) {
|
||||
updatedData[property] = data[property]
|
||||
}
|
||||
if (property == 'animation1threshold' && data[property]) {
|
||||
updatedData[property] = parseInt(data[property])
|
||||
}
|
||||
if (property == 'animation2threshold' && data[property]) {
|
||||
updatedData[property] = parseInt(data[property])
|
||||
}
|
||||
if (property == 'animation3threshold' && data[property]) {
|
||||
updatedData[property] = parseInt(data[property])
|
||||
}
|
||||
}
|
||||
LNbits.api
|
||||
.request('POST', '/copilot/api/v1/copilot', wallet, updatedData)
|
||||
.then(function (response) {
|
||||
self.CopilotLinks.push(mapCopilot(response.data))
|
||||
self.formDialogCopilot.show = false
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getCopilots: function () {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/copilot/api/v1/copilot',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.CopilotLinks = response.data.map(mapCopilot)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getCopilot: function (copilot_id) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/copilot/api/v1/copilot/' + copilot_id,
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
localStorage.setItem('copilot', JSON.stringify(response.data))
|
||||
localStorage.setItem('inkey', self.g.user.wallets[0].inkey)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
openCopilotCompose: function (copilot_id) {
|
||||
this.getCopilot(copilot_id)
|
||||
let params =
|
||||
'scrollbars=no, resizable=no,status=no,location=no,toolbar=no,menubar=no,width=1200,height=644,left=410,top=100'
|
||||
open('../copilot/cp/', '_blank', params)
|
||||
},
|
||||
openCopilotPanel: function (copilot_id) {
|
||||
this.getCopilot(copilot_id)
|
||||
let params =
|
||||
'scrollbars=no, resizable=no,status=no,location=no,toolbar=no,menubar=no,width=300,height=450,left=10,top=400'
|
||||
open('../copilot/pn/', '_blank', params)
|
||||
},
|
||||
deleteCopilotLink: function (copilotId) {
|
||||
var self = this
|
||||
var link = _.findWhere(this.CopilotLinks, {id: copilotId})
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this pay link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/copilot/api/v1/copilot/' + copilotId,
|
||||
self.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.CopilotLinks = _.reject(self.CopilotLinks, function (obj) {
|
||||
return obj.id === copilotId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
openUpdateCopilotLink: function (copilotId) {
|
||||
var self = this
|
||||
var copilot = _.findWhere(this.CopilotLinks, {id: copilotId})
|
||||
self.formDialogCopilot.data = _.clone(copilot._data)
|
||||
self.formDialogCopilot.show = true
|
||||
},
|
||||
updateCopilot: function (wallet, data) {
|
||||
var self = this
|
||||
var updatedData = {}
|
||||
for (const property in data) {
|
||||
if (data[property]) {
|
||||
updatedData[property] = data[property]
|
||||
}
|
||||
if (property == 'animation1threshold' && data[property]) {
|
||||
updatedData[property] = parseInt(data[property])
|
||||
}
|
||||
if (property == 'animation2threshold' && data[property]) {
|
||||
updatedData[property] = parseInt(data[property])
|
||||
}
|
||||
if (property == 'animation3threshold' && data[property]) {
|
||||
updatedData[property] = parseInt(data[property])
|
||||
}
|
||||
}
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/copilot/api/v1/copilot/' + updatedData.id,
|
||||
wallet,
|
||||
updatedData
|
||||
)
|
||||
.then(function (response) {
|
||||
self.CopilotLinks = _.reject(self.CopilotLinks, function (obj) {
|
||||
return obj.id === updatedData.id
|
||||
})
|
||||
self.CopilotLinks.push(mapCopilot(response.data))
|
||||
self.formDialogCopilot.show = false
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
exportcopilotCSV: function () {
|
||||
var self = this
|
||||
LNbits.utils.exportCSV(self.CopilotsTable.columns, this.CopilotLinks)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
var self = this
|
||||
var getCopilots = this.getCopilots
|
||||
getCopilots()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
157
lnbits/extensions/copilot/templates/copilot/panel.html
Normal file
|
@ -0,0 +1,157 @@
|
|||
{% extends "public.html" %} {% block page %}
|
||||
<div class="q-pa-sm" style="width: 240px; margin: 10px auto">
|
||||
<q-card class="my-card">
|
||||
<div class="column">
|
||||
<div class="col">
|
||||
<center>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
@click="openCompose"
|
||||
icon="face"
|
||||
style="font-size: 60px"
|
||||
></q-btn>
|
||||
</center>
|
||||
</div>
|
||||
<center>
|
||||
<div class="col" style="margin: 15px; font-size: 22px">
|
||||
Title: {% raw %} {{ copilot.title }} {% endraw %}
|
||||
</div>
|
||||
</center>
|
||||
<q-separator></q-separator>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
class="q-mt-sm q-ml-sm"
|
||||
color="primary"
|
||||
@click="fullscreenToggle"
|
||||
label="Screen share"
|
||||
size="sm"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-pa-sm">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
style="width: 95%"
|
||||
color="primary"
|
||||
@click="animationBTN('rocket')"
|
||||
label="rocket"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn
|
||||
style="width: 95%"
|
||||
color="primary"
|
||||
@click="animationBTN('confetti')"
|
||||
label="confetti"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn
|
||||
style="width: 95%"
|
||||
color="primary"
|
||||
@click="animationBTN('face')"
|
||||
label="face"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-pa-sm">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
style="width: 95%"
|
||||
color="primary"
|
||||
@click="animationBTN('rick')"
|
||||
label="rick"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn
|
||||
style="width: 95%"
|
||||
color="primary"
|
||||
@click="animationBTN('martijn')"
|
||||
label="martijn"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn
|
||||
style="width: 95%"
|
||||
color="primary"
|
||||
@click="animationBTN('bitcoin')"
|
||||
label="bitcoin"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data() {
|
||||
return {
|
||||
fullscreen_cam: true,
|
||||
textareaModel: '',
|
||||
iframe: '',
|
||||
copilot: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
iframeChange: function (url) {
|
||||
this.connection.send(String(url))
|
||||
},
|
||||
fullscreenToggle: function () {
|
||||
self = this
|
||||
self.animationBTN(String(this.fullscreen_cam))
|
||||
if (this.fullscreen_cam) {
|
||||
this.fullscreen_cam = false
|
||||
} else {
|
||||
this.fullscreen_cam = true
|
||||
}
|
||||
},
|
||||
openCompose: function () {
|
||||
let params =
|
||||
'scrollbars=no, resizable=no,status=no,location=no,toolbar=no,menubar=no,width=1200,height=644,left=410,top=100'
|
||||
open('../cp/', 'test', params)
|
||||
},
|
||||
animationBTN: function (name) {
|
||||
self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/copilot/api/v1/copilot/ws/' + self.copilot.id + '/none/' + name
|
||||
)
|
||||
.then(function (response1) {
|
||||
self.$q.notify({
|
||||
color: 'green',
|
||||
message: 'Sent!'
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
self = this
|
||||
self.copilot = JSON.parse(localStorage.getItem('copilot'))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
61
lnbits/extensions/copilot/views.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
from quart import g, abort, render_template, jsonify, websocket
|
||||
from http import HTTPStatus
|
||||
import httpx
|
||||
from collections import defaultdict
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from . import copilot_ext
|
||||
from .crud import get_copilot
|
||||
from quart import g, abort, render_template, jsonify, websocket
|
||||
from functools import wraps
|
||||
import trio
|
||||
import shortuuid
|
||||
from . import copilot_ext
|
||||
|
||||
|
||||
@copilot_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
async def index():
|
||||
return await render_template("copilot/index.html", user=g.user)
|
||||
|
||||
|
||||
@copilot_ext.route("/cp/")
|
||||
async def compose():
|
||||
return await render_template("copilot/compose.html")
|
||||
|
||||
|
||||
@copilot_ext.route("/pn/")
|
||||
async def panel():
|
||||
return await render_template("copilot/panel.html")
|
||||
|
||||
|
||||
##################WEBSOCKET ROUTES########################
|
||||
|
||||
# socket_relay is a list where the control panel or
|
||||
# lnurl endpoints can leave a message for the compose window
|
||||
|
||||
connected_websockets = defaultdict(set)
|
||||
|
||||
|
||||
@copilot_ext.websocket("/ws/<id>/")
|
||||
async def wss(id):
|
||||
copilot = await get_copilot(id)
|
||||
if not copilot:
|
||||
return "", HTTPStatus.FORBIDDEN
|
||||
global connected_websockets
|
||||
send_channel, receive_channel = trio.open_memory_channel(0)
|
||||
connected_websockets[id].add(send_channel)
|
||||
try:
|
||||
while True:
|
||||
data = await receive_channel.receive()
|
||||
await websocket.send(data)
|
||||
finally:
|
||||
connected_websockets[id].remove(send_channel)
|
||||
|
||||
|
||||
async def updater(copilot_id, data, comment):
|
||||
copilot = await get_copilot(copilot_id)
|
||||
if not copilot:
|
||||
return
|
||||
for queue in connected_websockets[copilot_id]:
|
||||
await queue.send(f"{data + '-' + comment}")
|
109
lnbits/extensions/copilot/views_api.py
Normal file
|
@ -0,0 +1,109 @@
|
|||
import hashlib
|
||||
from quart import g, jsonify, url_for, websocket
|
||||
from http import HTTPStatus
|
||||
import httpx
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from .views import updater
|
||||
|
||||
from . import copilot_ext
|
||||
|
||||
from lnbits.extensions.copilot import copilot_ext
|
||||
from .crud import (
|
||||
create_copilot,
|
||||
update_copilot,
|
||||
get_copilot,
|
||||
get_copilots,
|
||||
delete_copilot,
|
||||
)
|
||||
|
||||
#######################COPILOT##########################
|
||||
|
||||
|
||||
@copilot_ext.route("/api/v1/copilot", methods=["POST"])
|
||||
@copilot_ext.route("/api/v1/copilot/<copilot_id>", methods=["PUT"])
|
||||
@api_check_wallet_key("admin")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"title": {"type": "string", "empty": False, "required": True},
|
||||
"lnurl_toggle": {"type": "integer", "empty": False},
|
||||
"wallet": {"type": "string", "empty": False, "required": False},
|
||||
"animation1": {"type": "string", "empty": True, "required": False},
|
||||
"animation2": {"type": "string", "empty": True, "required": False},
|
||||
"animation3": {"type": "string", "empty": True, "required": False},
|
||||
"animation1threshold": {"type": "integer", "empty": True, "required": False},
|
||||
"animation2threshold": {"type": "integer", "empty": True, "required": False},
|
||||
"animation3threshold": {"type": "integer", "empty": True, "required": False},
|
||||
"animation1webhook": {"type": "string", "empty": True, "required": False},
|
||||
"animation2webhook": {"type": "string", "empty": True, "required": False},
|
||||
"animation3webhook": {"type": "string", "empty": True, "required": False},
|
||||
"lnurl_title": {"type": "string", "empty": True, "required": False},
|
||||
"show_message": {"type": "integer", "empty": True, "required": False},
|
||||
"show_ack": {"type": "integer", "empty": True},
|
||||
"show_price": {"type": "string", "empty": True},
|
||||
}
|
||||
)
|
||||
async def api_copilot_create_or_update(copilot_id=None):
|
||||
if not copilot_id:
|
||||
copilot = await create_copilot(user=g.wallet.user, **g.data)
|
||||
return jsonify(copilot._asdict()), HTTPStatus.CREATED
|
||||
else:
|
||||
copilot = await update_copilot(copilot_id=copilot_id, **g.data)
|
||||
return jsonify(copilot._asdict()), HTTPStatus.OK
|
||||
|
||||
|
||||
@copilot_ext.route("/api/v1/copilot", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_copilots_retrieve():
|
||||
try:
|
||||
return (
|
||||
jsonify(
|
||||
[{**copilot._asdict()} for copilot in await get_copilots(g.wallet.user)]
|
||||
),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
except:
|
||||
return ""
|
||||
|
||||
|
||||
@copilot_ext.route("/api/v1/copilot/<copilot_id>", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
async def api_copilot_retrieve(copilot_id):
|
||||
copilot = await get_copilot(copilot_id)
|
||||
if not copilot:
|
||||
return jsonify({"message": "copilot does not exist"}), HTTPStatus.NOT_FOUND
|
||||
if not copilot.lnurl_toggle:
|
||||
return (
|
||||
jsonify({**copilot._asdict()}),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
return (
|
||||
jsonify({**copilot._asdict(), **{"lnurl": copilot.lnurl}}),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
|
||||
|
||||
@copilot_ext.route("/api/v1/copilot/<copilot_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("admin")
|
||||
async def api_copilot_delete(copilot_id):
|
||||
copilot = await get_copilot(copilot_id)
|
||||
|
||||
if not copilot:
|
||||
return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
await delete_copilot(copilot_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
@copilot_ext.route("/api/v1/copilot/ws/<copilot_id>/<comment>/<data>", methods=["GET"])
|
||||
async def api_copilot_ws_relay(copilot_id, comment, data):
|
||||
copilot = await get_copilot(copilot_id)
|
||||
if not copilot:
|
||||
return jsonify({"message": "copilot does not exist"}), HTTPStatus.NOT_FOUND
|
||||
try:
|
||||
await updater(copilot_id, data, comment)
|
||||
except:
|
||||
return "", HTTPStatus.FORBIDDEN
|
||||
return "", HTTPStatus.OK
|
|
@ -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,))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="productDialog.show = true"
|
||||
<q-btn unelevated color="primary" @click="productDialog.show = true"
|
||||
>New Product</q-btn
|
||||
>
|
||||
<q-btn unelevated color="deep-purple" @click="indexerDialog.show = true"
|
||||
<q-btn unelevated color="primary" @click="indexerDialog.show = true"
|
||||
>New Indexer
|
||||
<q-tooltip>
|
||||
Frontend shop your stall will list its products in
|
||||
|
@ -282,7 +282,7 @@
|
|||
<q-btn
|
||||
v-if="productDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Update Product</q-btn
|
||||
>
|
||||
|
@ -290,7 +290,7 @@
|
|||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="productDialog.data.image == null
|
||||
|| productDialog.data.product == null
|
||||
|| productDialog.data.description == null
|
||||
|
@ -374,7 +374,7 @@
|
|||
<q-btn
|
||||
v-if="indexerDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Update Indexer</q-btn
|
||||
>
|
||||
|
@ -382,7 +382,7 @@
|
|||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="indexerDialog.data.shopname == null
|
||||
|| indexerDialog.data.shippingzone1 == null
|
||||
|| indexerDialog.data.indexeraddress == null
|
||||
|
|
|
@ -230,7 +230,7 @@ async def api_diagonalley_order_delete(order_id):
|
|||
async def api_diagonalleys_order_paid(order_id):
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute(
|
||||
"UPDATE orders SET paid = ? WHERE id = ?",
|
||||
"UPDATE diagonalley.orders SET paid = ? WHERE id = ?",
|
||||
(
|
||||
True,
|
||||
order_id,
|
||||
|
@ -244,13 +244,15 @@ async def api_diagonalleys_order_paid(order_id):
|
|||
async def api_diagonalleys_order_shipped(order_id):
|
||||
with open_ext_db("diagonalley") as db:
|
||||
db.execute(
|
||||
"UPDATE orders SET shipped = ? WHERE id = ?",
|
||||
"UPDATE diagonalley.orders SET shipped = ? WHERE id = ?",
|
||||
(
|
||||
True,
|
||||
order_id,
|
||||
),
|
||||
)
|
||||
order = db.fetchone("SELECT * FROM orders WHERE id = ?", (order_id,))
|
||||
order = db.fetchone(
|
||||
"SELECT * FROM diagonalley.orders WHERE id = ?", (order_id,)
|
||||
)
|
||||
|
||||
return (
|
||||
jsonify(
|
||||
|
@ -268,12 +270,16 @@ async def api_diagonalleys_order_shipped(order_id):
|
|||
)
|
||||
async def api_diagonalleys_stall_products(indexer_id):
|
||||
with open_ext_db("diagonalley") as db:
|
||||
rows = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
|
||||
rows = db.fetchone(
|
||||
"SELECT * FROM diagonalley.indexers WHERE id = ?", (indexer_id,)
|
||||
)
|
||||
print(rows[1])
|
||||
if not rows:
|
||||
return jsonify({"message": "Indexer does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
products = db.fetchone("SELECT * FROM products WHERE wallet = ?", (rows[1],))
|
||||
products = db.fetchone(
|
||||
"SELECT * FROM diagonalley.products WHERE wallet = ?", (rows[1],)
|
||||
)
|
||||
if not products:
|
||||
return jsonify({"message": "No products"}), HTTPStatus.NOT_FOUND
|
||||
|
||||
|
@ -293,7 +299,9 @@ async def api_diagonalleys_stall_products(indexer_id):
|
|||
)
|
||||
async def api_diagonalleys_stall_checkshipped(checking_id):
|
||||
with open_ext_db("diagonalley") as db:
|
||||
rows = db.fetchone("SELECT * FROM orders WHERE invoiceid = ?", (checking_id,))
|
||||
rows = db.fetchone(
|
||||
"SELECT * FROM diagonalley.orders WHERE invoiceid = ?", (checking_id,)
|
||||
)
|
||||
|
||||
return jsonify({"shipped": rows["shipped"]}), HTTPStatus.OK
|
||||
|
||||
|
@ -329,7 +337,7 @@ async def api_diagonalley_stall_order(indexer_id):
|
|||
with open_ext_db("diagonalley") as db:
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
|
|
|
@ -14,7 +14,7 @@ async def create_ticket(
|
|||
) -> 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]
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="formDialog.data.name == '' || formDialog.data.email == '' || paymentReq"
|
||||
type="submit"
|
||||
>Submit</q-btn
|
||||
|
@ -46,7 +46,7 @@
|
|||
size="xl"
|
||||
:href="ticketLink.data.link"
|
||||
target="_blank"
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
type="a"
|
||||
>Link to your ticket!</q-btn
|
||||
>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
|
||||
<q-btn unelevated color="primary" @click="formDialog.show = true"
|
||||
>New Event</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
|
@ -267,14 +267,14 @@
|
|||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Update Event</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="formDialog.data.wallet == null || formDialog.data.name == null || formDialog.data.info == null || formDialog.data.closing_date == null || formDialog.data.event_start_date == null || formDialog.data.event_end_date == null || formDialog.data.amount_tickets == null || formDialog.data.price_per_ticket == null"
|
||||
type="submit"
|
||||
>Create Event</q-btn
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<br />
|
||||
|
||||
<q-btn unelevated color="deep-purple" @click="showCamera" size="xl"
|
||||
<q-btn unelevated color="primary" @click="showCamera" size="xl"
|
||||
>Scan ticket</q-btn
|
||||
>
|
||||
</center>
|
||||
|
@ -82,7 +82,7 @@
|
|||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
Vue.use(VueQrcodeReader)
|
||||
var mapEvents = function(obj) {
|
||||
var mapEvents = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
|
@ -94,7 +94,7 @@
|
|||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function() {
|
||||
data: function () {
|
||||
return {
|
||||
tickets: [],
|
||||
ticketsTable: {
|
||||
|
@ -119,35 +119,35 @@
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
hoverEmail: function(tmp) {
|
||||
hoverEmail: function (tmp) {
|
||||
this.tickets.data.emailtemp = tmp
|
||||
},
|
||||
closeCamera: function() {
|
||||
closeCamera: function () {
|
||||
this.sendCamera.show = false
|
||||
},
|
||||
showCamera: function() {
|
||||
showCamera: function () {
|
||||
this.sendCamera.show = true
|
||||
},
|
||||
decodeQR: function(res) {
|
||||
decodeQR: function (res) {
|
||||
this.sendCamera.show = false
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request('GET', '/events/api/v1/register/ticket/' + res)
|
||||
.then(function(response) {
|
||||
.then(function (response) {
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Registered!'
|
||||
})
|
||||
setTimeout(function() {
|
||||
setTimeout(function () {
|
||||
window.location.reload()
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function(error) {
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getEventTickets: function() {
|
||||
getEventTickets: function () {
|
||||
var self = this
|
||||
console.log('obj')
|
||||
LNbits.api
|
||||
|
@ -155,17 +155,17 @@
|
|||
'GET',
|
||||
'/events/api/v1/eventtickets/{{ wallet_id }}/{{ event_id }}'
|
||||
)
|
||||
.then(function(response) {
|
||||
self.tickets = response.data.map(function(obj) {
|
||||
.then(function (response) {
|
||||
self.tickets = response.data.map(function (obj) {
|
||||
return mapEvents(obj)
|
||||
})
|
||||
})
|
||||
.catch(function(error) {
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
created: function () {
|
||||
this.getEventTickets()
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
# async def m001_initial(db):
|
||||
|
||||
# await db.execute(
|
||||
# """
|
||||
# CREATE TABLE IF NOT EXISTS example (
|
||||
# f"""
|
||||
# CREATE TABLE example.example (
|
||||
# id TEXT PRIMARY KEY,
|
||||
# wallet TEXT NOT NULL,
|
||||
# time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
# time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
# );
|
||||
# """
|
||||
# )
|
||||
|
|
3
lnbits/extensions/hivemind/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
<h1>Hivemind</h1>
|
||||
|
||||
Placeholder for a future <a href="https://bitcoinhivemind.com/">Bitcoin Hivemind</a> extension.
|
11
lnbits/extensions/hivemind/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from quart import Blueprint
|
||||
from lnbits.db import Database
|
||||
|
||||
db = Database("ext_hivemind")
|
||||
|
||||
hivemind_ext: Blueprint = Blueprint(
|
||||
"hivemind", __name__, static_folder="static", template_folder="templates"
|
||||
)
|
||||
|
||||
|
||||
from .views import * # noqa
|
6
lnbits/extensions/hivemind/config.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "Hivemind",
|
||||
"short_description": "Make cheap talk expensive!",
|
||||
"icon": "batch_prediction",
|
||||
"contributors": ["fiatjaf"]
|
||||
}
|
10
lnbits/extensions/hivemind/migrations.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
# async def m001_initial(db):
|
||||
# await db.execute(
|
||||
# f"""
|
||||
# CREATE TABLE hivemind.hivemind (
|
||||
# id TEXT PRIMARY KEY,
|
||||
# wallet TEXT NOT NULL,
|
||||
# time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
# );
|
||||
# """
|
||||
# )
|
11
lnbits/extensions/hivemind/models.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
# from sqlite3 import Row
|
||||
# from typing import NamedTuple
|
||||
|
||||
|
||||
# class Example(NamedTuple):
|
||||
# id: str
|
||||
# wallet: str
|
||||
#
|
||||
# @classmethod
|
||||
# def from_row(cls, row: Row) -> "Example":
|
||||
# return cls(**dict(row))
|
35
lnbits/extensions/hivemind/templates/hivemind/index.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-mt-none q-mb-md">
|
||||
This extension is just a placeholder for now.
|
||||
</h5>
|
||||
<p>
|
||||
<a href="https://bitcoinhivemind.com/">Hivemind</a> is a Bitcoin sidechain
|
||||
project for a peer-to-peer oracle protocol that absorbs accurate data into
|
||||
a blockchain so that Bitcoin users can speculate in prediction markets.
|
||||
</p>
|
||||
<p>
|
||||
These markets have the potential to revolutionize the emergence of
|
||||
diffusion of knowledge in society and fix all sorts of problems in the
|
||||
world.
|
||||
</p>
|
||||
<p>
|
||||
This extension will become fully operative when the
|
||||
<a href="https://drivechain.xyz/">BIP300</a> soft-fork gets activated and
|
||||
Bitcoin Hivemind is launched.
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
12
lnbits/extensions/hivemind/views.py
Normal file
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -46,12 +46,6 @@ new Vue({
|
|||
align: 'left',
|
||||
label: 'Price',
|
||||
field: 'price'
|
||||
},
|
||||
{
|
||||
name: 'profit',
|
||||
align: 'left',
|
||||
label: 'Profit',
|
||||
field: 'profit'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
|
@ -93,7 +87,11 @@ new Vue({
|
|||
getJukeboxes() {
|
||||
self = this
|
||||
LNbits.api
|
||||
.request('GET', '/jukebox/api/v1/jukebox', self.g.user.wallets[0].adminkey)
|
||||
.request(
|
||||
'GET',
|
||||
'/jukebox/api/v1/jukebox',
|
||||
self.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.JukeboxLinks = response.data.map(mapJukebox)
|
||||
})
|
||||
|
@ -165,10 +163,10 @@ new Vue({
|
|||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
authAccess() {
|
||||
authAccess() {
|
||||
self = this
|
||||
self.requestAuthorization()
|
||||
self.getSpotifyTokens()
|
||||
self.requestAuthorization()
|
||||
self.getSpotifyTokens()
|
||||
self.$q.notify({
|
||||
spinner: true,
|
||||
message: 'Processing',
|
||||
|
@ -195,37 +193,37 @@ new Vue({
|
|||
if (self.jukeboxDialog.data.sp_access_token) {
|
||||
self.refreshPlaylists()
|
||||
self.refreshDevices()
|
||||
console.log("this.devices")
|
||||
console.log('this.devices')
|
||||
console.log(self.devices)
|
||||
console.log("this.devices")
|
||||
console.log('this.devices')
|
||||
setTimeout(function () {
|
||||
if (self.devices.length < 1 || self.playlists.length < 1) {
|
||||
self.$q.notify({
|
||||
spinner: true,
|
||||
color: 'red',
|
||||
message:
|
||||
'Error! Make sure Spotify is open on the device you wish to use, has playlists, and is playing something',
|
||||
timeout: 10000
|
||||
})
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/jukebox/api/v1/jukebox/' + response.data.id,
|
||||
self.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.getJukeboxes()
|
||||
if (self.devices.length < 1 || self.playlists.length < 1) {
|
||||
self.$q.notify({
|
||||
spinner: true,
|
||||
color: 'red',
|
||||
message:
|
||||
'Error! Make sure Spotify is open on the device you wish to use, has playlists, and is playing something',
|
||||
timeout: 10000
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
clearInterval(timerId)
|
||||
self.closeFormDialog()
|
||||
} else {
|
||||
self.step = 4
|
||||
clearInterval(timerId)
|
||||
}
|
||||
}, 2000)
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/jukebox/api/v1/jukebox/' + response.data.id,
|
||||
self.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.getJukeboxes()
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
clearInterval(timerId)
|
||||
self.closeFormDialog()
|
||||
} else {
|
||||
self.step = 4
|
||||
clearInterval(timerId)
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -347,15 +345,15 @@ new Vue({
|
|||
}
|
||||
}
|
||||
},
|
||||
refreshDevices() {
|
||||
refreshDevices() {
|
||||
self = this
|
||||
self.deviceApi(
|
||||
self.deviceApi(
|
||||
'GET',
|
||||
'https://api.spotify.com/v1/me/player/devices',
|
||||
null
|
||||
)
|
||||
},
|
||||
fetchAccessToken(code) {
|
||||
fetchAccessToken(code) {
|
||||
self = this
|
||||
let body = 'grant_type=authorization_code'
|
||||
body += '&code=' + code
|
||||
|
@ -363,16 +361,16 @@ new Vue({
|
|||
'&redirect_uri=' +
|
||||
encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_id)
|
||||
|
||||
self.callAuthorizationApi(body)
|
||||
self.callAuthorizationApi(body)
|
||||
},
|
||||
refreshAccessToken() {
|
||||
refreshAccessToken() {
|
||||
self = this
|
||||
let body = 'grant_type=refresh_token'
|
||||
body += '&refresh_token=' + self.jukeboxDialog.data.sp_refresh_token
|
||||
body += '&client_id=' + self.jukeboxDialog.data.sp_user
|
||||
self.callAuthorizationApi(body)
|
||||
self.callAuthorizationApi(body)
|
||||
},
|
||||
callAuthorizationApi(body) {
|
||||
callAuthorizationApi(body) {
|
||||
self = this
|
||||
console.log(
|
||||
btoa(
|
||||
|
|
|
@ -6,14 +6,9 @@ new Vue({
|
|||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
|
||||
},
|
||||
created() {
|
||||
|
||||
}
|
||||
methods: {},
|
||||
created() {}
|
||||
})
|
||||
|
|
28
lnbits/extensions/jukebox/tasks.py
Normal file
|
@ -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)
|
|
@ -1,24 +1,33 @@
|
|||
<q-card-section>
|
||||
To use this extension you need a Spotify client ID and client secret. You
|
||||
get these by creating an app in the Spotify developers dashboard
|
||||
<a style="color:#43a047" href="https://developer.spotify.com/dashboard/applications">here </a>
|
||||
<br /><br />Select the playlists you want people to be able to pay for,
|
||||
share the frontend page, profit :) <br /><br />
|
||||
Made by, <a style="color:#43a047" href="https://twitter.com/arcbtc">benarc</a>. Inspired by,
|
||||
<a style="color:#43a047" href="https://twitter.com/pirosb3/status/1056263089128161280">pirosb3</a>.
|
||||
To use this extension you need a Spotify client ID and client secret. You get
|
||||
these by creating an app in the Spotify developers dashboard
|
||||
<a
|
||||
style="color: #43a047"
|
||||
href="https://developer.spotify.com/dashboard/applications"
|
||||
>here
|
||||
</a>
|
||||
<br /><br />Select the playlists you want people to be able to pay for, share
|
||||
the frontend page, profit :) <br /><br />
|
||||
Made by,
|
||||
<a style="color: #43a047" href="https://twitter.com/arcbtc">benarc</a>.
|
||||
Inspired by,
|
||||
<a
|
||||
style="color: #43a047"
|
||||
href="https://twitter.com/pirosb3/status/1056263089128161280"
|
||||
>pirosb3</a
|
||||
>.
|
||||
</q-card-section>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<q-expansion-item group="extras" icon="swap_vertical_circle" label="API info" :content-inset-level="0.5">
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" dense expand-separator label="List jukeboxes">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-blue">GET</span>
|
||||
/jukebox/api/v1/jukebox</code>
|
||||
<code><span class="text-blue">GET</span> /jukebox/api/v1/jukebox</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
|
@ -27,7 +36,8 @@
|
|||
</h5>
|
||||
<code>[<jukebox_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X GET {{ request.url_root }}api/v1/jukebox -H "X-Api-Key: {{
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/jukebox -H "X-Api-Key: {{
|
||||
g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
|
@ -36,8 +46,10 @@
|
|||
<q-expansion-item group="api" dense expand-separator label="Get jukebox">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-blue">GET</span>
|
||||
/jukebox/api/v1/jukebox/<juke_id></code>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/jukebox/api/v1/jukebox/<juke_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
|
@ -46,36 +58,44 @@
|
|||
</h5>
|
||||
<code><jukebox_object></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X GET {{ request.url_root }}api/v1/jukebox/<juke_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].adminkey }}"
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/jukebox/<juke_id> -H
|
||||
"X-Api-Key: {{ g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Create/update track">
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Create/update track"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-green">POST/PUT</span>
|
||||
/jukebox/api/v1/jukebox/</code>
|
||||
<code
|
||||
><span class="text-green">POST/PUT</span>
|
||||
/jukebox/api/v1/jukebox/</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Body (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code><jukbox_object></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X POST {{ request.url_root }}api/v1/jukebox/ -d
|
||||
'{"user": <string, user_id>,
|
||||
"title": <string>, "wallet":<string>, "sp_user":
|
||||
<string, spotify_user_account>, "sp_secret": <string, spotify_user_secret>, "sp_access_token":
|
||||
<string, not_required>, "sp_refresh_token":
|
||||
<string, not_required>, "sp_device": <string, spotify_user_secret>, "sp_playlists":
|
||||
<string, not_required>, "price":
|
||||
<integer, not_required>}' -H "Content-type:
|
||||
application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}"
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}api/v1/jukebox/ -d '{"user":
|
||||
<string, user_id>, "title": <string>,
|
||||
"wallet":<string>, "sp_user": <string,
|
||||
spotify_user_account>, "sp_secret": <string,
|
||||
spotify_user_secret>, "sp_access_token": <string,
|
||||
not_required>, "sp_refresh_token": <string, not_required>,
|
||||
"sp_device": <string, spotify_user_secret>, "sp_playlists":
|
||||
<string, not_required>, "price": <integer, not_required>}'
|
||||
-H "Content-type: application/json" -H "X-Api-Key:
|
||||
{{g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
@ -83,8 +103,10 @@
|
|||
<q-expansion-item group="api" dense expand-separator label="Delete jukebox">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-red">DELETE</span>
|
||||
/jukebox/api/v1/jukebox/<juke_id></code>
|
||||
<code
|
||||
><span class="text-red">DELETE</span>
|
||||
/jukebox/api/v1/jukebox/<juke_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
|
@ -93,9 +115,11 @@
|
|||
</h5>
|
||||
<code><jukebox_object></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code>curl -X DELETE {{ request.url_root }}api/v1/jukebox/<juke_id> -H "X-Api-Key: {{
|
||||
g.user.wallets[0].adminkey }}"
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root }}api/v1/jukebox/<juke_id>
|
||||
-H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item></q-expansion-item
|
||||
>
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
style="font-size: 20rem"
|
||||
></q-icon>
|
||||
|
||||
<h5 class="q-my-none">Ask the host to turn on the device and launch spotify</h5>
|
||||
<h5 class="q-my-none">
|
||||
Ask the host to turn on the device and launch spotify
|
||||
</h5>
|
||||
<br />
|
||||
</center>
|
||||
</q-card-section>
|
||||
|
|
|
@ -4,18 +4,36 @@
|
|||
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="green-7" class="q-ma-lg" @click="openNewDialog()">Add Spotify Jukebox</q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
class="q-ma-lg"
|
||||
@click="openNewDialog()"
|
||||
>Add Spotify Jukebox</q-btn
|
||||
>
|
||||
|
||||
{% raw %}
|
||||
|
||||
<q-table flat dense :data="JukeboxLinks" row-key="id" :columns="JukeboxTable.columns"
|
||||
:pagination.sync="JukeboxTable.pagination" :filter="filter">
|
||||
<q-table
|
||||
flat
|
||||
dense
|
||||
:data="JukeboxLinks"
|
||||
row-key="id"
|
||||
:columns="JukeboxTable.columns"
|
||||
:pagination.sync="JukeboxTable.pagination"
|
||||
:filter="filter"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th auto-width></q-th>
|
||||
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props" auto-width>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
auto-width
|
||||
>
|
||||
<div v-if="col.name == 'id'"></div>
|
||||
<div v-else>{{ col.label }}</div>
|
||||
</q-th>
|
||||
|
@ -26,18 +44,43 @@
|
|||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn unelevated dense size="xs" icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openQrCodeDialog(props.row.sp_id)">
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="launch"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openQrCodeDialog(props.row.sp_id)"
|
||||
>
|
||||
<q-tooltip> Jukebox QR </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn flat dense size="xs" @click="updateJukebox(props.row.id)" icon="edit" color="light-blue"></q-btn>
|
||||
<q-btn flat dense size="xs" @click="deleteJukebox(props.row.id)" icon="cancel" color="pink">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="updateJukebox(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteJukebox(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
>
|
||||
<q-tooltip> Delete Jukebox </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props" auto-width>
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
auto-width
|
||||
>
|
||||
<div v-if="col.name == 'id'"></div>
|
||||
<div v-else>{{ col.value }}</div>
|
||||
</q-td>
|
||||
|
@ -63,23 +106,62 @@
|
|||
|
||||
<q-dialog v-model="jukeboxDialog.show" position="top" @hide="closeFormDialog">
|
||||
<q-card class="q-pa-md q-pt-lg q-mt-md" style="width: 100%">
|
||||
<q-stepper v-model="step" active-color="green-7" inactive-color="green-10" vertical animated>
|
||||
<q-step :name="1" title="Pick wallet, price" icon="account_balance_wallet" :done="step > 1">
|
||||
<q-input filled class="q-pt-md" dense v-model.trim="jukeboxDialog.data.title" label="Jukebox name"></q-input>
|
||||
<q-select class="q-pb-md q-pt-md" filled dense emit-value v-model="jukeboxDialog.data.wallet"
|
||||
:options="g.user.walletOptions" label="Wallet to use"></q-select>
|
||||
<q-input filled dense v-model.trim="jukeboxDialog.data.price" type="number" max="1440" label="Price per track"
|
||||
class="q-pb-lg">
|
||||
<q-stepper
|
||||
v-model="step"
|
||||
active-color="primary"
|
||||
inactive-color="secondary"
|
||||
vertical
|
||||
animated
|
||||
>
|
||||
<q-step
|
||||
:name="1"
|
||||
title="Pick wallet, price"
|
||||
icon="account_balance_wallet"
|
||||
:done="step > 1"
|
||||
>
|
||||
<q-input
|
||||
filled
|
||||
class="q-pt-md"
|
||||
dense
|
||||
v-model.trim="jukeboxDialog.data.title"
|
||||
label="Jukebox name"
|
||||
></q-input>
|
||||
<q-select
|
||||
class="q-pb-md q-pt-md"
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="jukeboxDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet to use"
|
||||
></q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="jukeboxDialog.data.price"
|
||||
type="number"
|
||||
max="1440"
|
||||
label="Price per track"
|
||||
class="q-pb-lg"
|
||||
>
|
||||
</q-input>
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<q-btn
|
||||
v-if="jukeboxDialog.data.title != null && jukeboxDialog.data.price != null && jukeboxDialog.data.wallet != null"
|
||||
color="green-7" @click="step = 2">Continue</q-btn>
|
||||
<q-btn v-else color="green-7" disable>Continue</q-btn>
|
||||
color="primary"
|
||||
@click="step = 2"
|
||||
>Continue</q-btn
|
||||
>
|
||||
<q-btn v-else color="primary" disable>Continue</q-btn>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-btn color="green-7" class="float-right" @click="closeFormDialog">Cancel</q-btn>
|
||||
<q-btn
|
||||
color="primary"
|
||||
class="float-right"
|
||||
@click="closeFormDialog"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -90,26 +172,57 @@
|
|||
<img src="/jukebox/static/spotapi.gif" />
|
||||
To use this extension you need a Spotify client ID and client secret.
|
||||
You get these by creating an app in the Spotify developers dashboard
|
||||
<a target="_blank" style="color:#43a047" href="https://developer.spotify.com/dashboard/applications">here</a>.
|
||||
<q-input filled class="q-pb-md q-pt-md" dense v-model.trim="jukeboxDialog.data.sp_user" label="Client ID">
|
||||
<a
|
||||
target="_blank"
|
||||
style="color: #43a047"
|
||||
href="https://developer.spotify.com/dashboard/applications"
|
||||
>here</a
|
||||
>.
|
||||
<q-input
|
||||
filled
|
||||
class="q-pb-md q-pt-md"
|
||||
dense
|
||||
v-model.trim="jukeboxDialog.data.sp_user"
|
||||
label="Client ID"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<q-input dense v-model="jukeboxDialog.data.sp_secret" filled :type="isPwd ? 'password' : 'text'"
|
||||
label="Client secret">
|
||||
<q-input
|
||||
dense
|
||||
v-model="jukeboxDialog.data.sp_secret"
|
||||
filled
|
||||
:type="isPwd ? 'password' : 'text'"
|
||||
label="Client secret"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer" @click="isPwd = !isPwd">
|
||||
<q-icon
|
||||
:name="isPwd ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="isPwd = !isPwd"
|
||||
>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-4">
|
||||
<q-btn v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched"
|
||||
color="green-7" @click="submitSpotifyKeys">Submit keys</q-btn>
|
||||
<q-btn v-else color="green-7" disable color="green-7">Submit keys</q-btn>
|
||||
<q-btn
|
||||
v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched"
|
||||
color="primary"
|
||||
@click="submitSpotifyKeys"
|
||||
>Submit keys</q-btn
|
||||
>
|
||||
<q-btn v-else color="primary" disable color="primary"
|
||||
>Submit keys</q-btn
|
||||
>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-btn color="green-7" class="float-right" @click="closeFormDialog">Cancel</q-btn>
|
||||
<q-btn
|
||||
color="primary"
|
||||
class="float-right"
|
||||
@click="closeFormDialog"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -120,42 +233,93 @@
|
|||
<img src="/jukebox/static/spotapi1.gif" />
|
||||
In the app go to edit-settings, set the redirect URI to this link
|
||||
<br />
|
||||
<q-btn dense outline unelevated color="green-7" size="xs"
|
||||
@click="copyText(locationcb + jukeboxDialog.data.sp_id, 'Link copied to clipboard!')">{% raw %}{{ locationcb
|
||||
}}{{ jukeboxDialog.data.sp_id }}{% endraw
|
||||
<q-btn
|
||||
dense
|
||||
outline
|
||||
unelevated
|
||||
color="primary"
|
||||
size="xs"
|
||||
@click="copyText(locationcb + jukeboxDialog.data.sp_id, 'Link copied to clipboard!')"
|
||||
>{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw
|
||||
%}<q-tooltip> Click to copy URL </q-tooltip>
|
||||
</q-btn>
|
||||
<br />
|
||||
Settings can be found
|
||||
<a target="_blank" style="color:#43a047" href="https://developer.spotify.com/dashboard/applications">here</a>.
|
||||
<a
|
||||
target="_blank"
|
||||
style="color: #43a047"
|
||||
href="https://developer.spotify.com/dashboard/applications"
|
||||
>here</a
|
||||
>.
|
||||
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-4">
|
||||
<q-btn v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched"
|
||||
color="green-7" @click="authAccess">Authorise access</q-btn>
|
||||
<q-btn v-else color="green-7" disable color="green-7">Authorise access</q-btn>
|
||||
<q-btn
|
||||
v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched"
|
||||
color="primary"
|
||||
@click="authAccess"
|
||||
>Authorise access</q-btn
|
||||
>
|
||||
<q-btn v-else color="primary" disable color="primary"
|
||||
>Authorise access</q-btn
|
||||
>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-btn color="green-7" class="float-right" @click="closeFormDialog">Cancel</q-btn>
|
||||
<q-btn
|
||||
color="primary"
|
||||
class="float-right"
|
||||
@click="closeFormDialog"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
</q-step>
|
||||
|
||||
<q-step :name="4" title="Select playlists" icon="queue_music" active-color="green-8" :done="step > 4">
|
||||
<q-select class="q-pb-md q-pt-md" filled dense emit-value v-model="jukeboxDialog.data.sp_device"
|
||||
:options="devices" label="Device jukebox will play to"></q-select>
|
||||
<q-select class="q-pb-md" filled dense multiple emit-value v-model="jukeboxDialog.data.sp_playlists"
|
||||
:options="playlists" label="Playlists available to the jukebox"></q-select>
|
||||
<q-step
|
||||
:name="4"
|
||||
title="Select playlists"
|
||||
icon="queue_music"
|
||||
active-color="primary"
|
||||
:done="step > 4"
|
||||
>
|
||||
<q-select
|
||||
class="q-pb-md q-pt-md"
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="jukeboxDialog.data.sp_device"
|
||||
:options="devices"
|
||||
label="Device jukebox will play to"
|
||||
></q-select>
|
||||
<q-select
|
||||
class="q-pb-md"
|
||||
filled
|
||||
dense
|
||||
multiple
|
||||
emit-value
|
||||
v-model="jukeboxDialog.data.sp_playlists"
|
||||
:options="playlists"
|
||||
label="Playlists available to the jukebox"
|
||||
></q-select>
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-5">
|
||||
<q-btn v-if="jukeboxDialog.data.sp_device != null && jukeboxDialog.data.sp_playlists != null"
|
||||
color="green-7" @click="createJukebox">Create Jukebox</q-btn>
|
||||
<q-btn v-else color="green-7" disable>Create Jukebox</q-btn>
|
||||
<q-btn
|
||||
v-if="jukeboxDialog.data.sp_device != null && jukeboxDialog.data.sp_playlists != null"
|
||||
color="primary"
|
||||
@click="createJukebox"
|
||||
>Create Jukebox</q-btn
|
||||
>
|
||||
<q-btn v-else color="primary" disable>Create Jukebox</q-btn>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<q-btn color="green-7" class="float-right" @click="closeFormDialog">Cancel</q-btn>
|
||||
<q-btn
|
||||
color="primary"
|
||||
class="float-right"
|
||||
@click="closeFormDialog"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</q-step>
|
||||
|
@ -169,15 +333,28 @@
|
|||
<h5 class="q-my-none">Shareable Jukebox QR</h5>
|
||||
</center>
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode :value="qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id" :options="{width: 800}"
|
||||
class="rounded-borders"></qrcode>
|
||||
<qrcode
|
||||
:value="qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn outline color="grey"
|
||||
@click="copyText(qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id, 'Link copied to clipboard!')">
|
||||
Copy jukebox link</q-btn>
|
||||
<q-btn outline color="grey" type="a" :href="qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id"
|
||||
target="_blank">Open jukebox</q-btn>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id, 'Link copied to clipboard!')"
|
||||
>
|
||||
Copy jukebox link</q-btn
|
||||
>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
type="a"
|
||||
:href="qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id"
|
||||
target="_blank"
|
||||
>Open jukebox</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
|
@ -186,4 +363,4 @@
|
|||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/pica@6.1.1/dist/pica.min.js"></script>
|
||||
<script src="/jukebox/static/js/index.js"></script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
<img style="width: 100px" :src="currentPlay.image" />
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<strong style="font-size: 20px">{{ currentPlay.name }}</strong><br />
|
||||
<strong style="font-size: 20px">{{ currentPlay.name }}</strong
|
||||
><br />
|
||||
<strong style="font-size: 15px">{{ currentPlay.artist }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,15 +20,30 @@
|
|||
<q-card class="q-mt-lg">
|
||||
<q-card-section>
|
||||
<p style="font-size: 22px">Pick a song</p>
|
||||
<q-select outlined v-model="playlist" :options="playlists" label="playlists" @input="selectPlaylist()">
|
||||
<q-select
|
||||
outlined
|
||||
v-model="playlist"
|
||||
:options="playlists"
|
||||
label="playlists"
|
||||
@input="selectPlaylist()"
|
||||
>
|
||||
</q-select>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-virtual-scroll style="max-height: 300px" :items="currentPlaylist" separator>
|
||||
<q-virtual-scroll
|
||||
style="max-height: 300px"
|
||||
:items="currentPlaylist"
|
||||
separator
|
||||
>
|
||||
<template v-slot="{ item, index }">
|
||||
<q-item :key="index" dense clickable v-ripple
|
||||
@click="payForSong(item.id, item.name, item.artist, item.image)">
|
||||
<q-item
|
||||
:key="index"
|
||||
dense
|
||||
clickable
|
||||
v-ripple
|
||||
@click="payForSong(item.id, item.name, item.artist, item.image)"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
{{ item.name }} - ({{ item.artist }})
|
||||
|
@ -55,7 +71,8 @@
|
|||
</q-card-section>
|
||||
<br />
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn outline color="grey" @click="getInvoice(receive.id)">Play for {% endraw %}{{ price }}{% raw %} sats
|
||||
<q-btn outline color="grey" @click="getInvoice(receive.id)"
|
||||
>Play for {% endraw %}{{ price }}{% raw %} sats
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
|
@ -63,10 +80,16 @@
|
|||
<q-dialog v-model="receive.dialogues.second" position="top">
|
||||
<q-card class="q-pa-lg lnbits__dialog-card">
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode :value="'lightning:' + receive.paymentReq" :options="{width: 800}" class="rounded-borders"></qrcode>
|
||||
<qrcode
|
||||
:value="'lightning:' + receive.paymentReq"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn outline color="grey" @click="copyText(receive.paymentReq)">Copy invoice</q-btn>
|
||||
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
|
||||
>Copy invoice</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
@ -84,7 +107,7 @@
|
|||
return {
|
||||
currentPlaylist: [],
|
||||
currentlyPlaying: {},
|
||||
cancelListener: () => { },
|
||||
cancelListener: () => {},
|
||||
playlists: {},
|
||||
playlist: '',
|
||||
heavyList: [],
|
||||
|
@ -111,14 +134,6 @@
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
cancelPayment: function () {
|
||||
this.paymentReq = null
|
||||
clearInterval(this.paymentDialog.checker)
|
||||
if (this.paymentDialog.dismissMsg) {
|
||||
this.paymentDialog.dismissMsg()
|
||||
}
|
||||
},
|
||||
closeReceiveDialog() { },
|
||||
payForSong(song_id, name, artist, image) {
|
||||
self = this
|
||||
self.receive.name = name
|
||||
|
@ -127,66 +142,69 @@
|
|||
self.receive.id = song_id
|
||||
self.receive.dialogues.first = true
|
||||
},
|
||||
startPaymentNotifier() {
|
||||
this.cancelListener()
|
||||
|
||||
this.cancelListener = LNbits.events.onInvoicePaid(
|
||||
this.selectedWallet,
|
||||
payment => {
|
||||
this.paid = true
|
||||
this.receive.dialogues.first = false
|
||||
this.receive.dialogues.second = false
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/jukebox/api/v1/jukebox/jb/invoicep/' +
|
||||
this.receive.id +
|
||||
'/{{ juke_id }}/' +
|
||||
this.receive.paymentHash
|
||||
)
|
||||
.then(response1 => {
|
||||
if (response1.data[2] == this.receive.id) {
|
||||
setTimeout(() => {
|
||||
this.getCurrent()
|
||||
}, 500)
|
||||
this.$q.notify({
|
||||
color: 'green',
|
||||
message:
|
||||
'Success! "' +
|
||||
this.receive.name +
|
||||
'" will be played soon',
|
||||
timeout: 3000
|
||||
})
|
||||
|
||||
this.paid = false
|
||||
response1 = []
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
self.paid = false
|
||||
response1 = []
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
getInvoice(song_id) {
|
||||
self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/jukebox/api/v1/jukebox/jb/invoice/' +
|
||||
'{{ juke_id }}' +
|
||||
'/' +
|
||||
song_id
|
||||
'{{ juke_id }}' +
|
||||
'/' +
|
||||
song_id
|
||||
)
|
||||
.then(function (response) {
|
||||
|
||||
self.receive.paymentReq = response.data[0][1]
|
||||
self.receive.paymentHash = response.data[0][0]
|
||||
self.receive.dialogues.second = true
|
||||
|
||||
var paymentChecker = setInterval(function () {
|
||||
if (!self.paid) {
|
||||
self.checkInvoice(self.receive.paymentHash, '{{ juke_id }}')
|
||||
}
|
||||
if (self.paid) {
|
||||
clearInterval(paymentChecker)
|
||||
self.paid = true
|
||||
self.receive.dialogues.first = false
|
||||
self.receive.dialogues.second = false
|
||||
self.$q.notify({
|
||||
message:
|
||||
'Processing',
|
||||
})
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/jukebox/api/v1/jukebox/jb/invoicep/' + song_id + '/{{ juke_id }}/' + self.receive.paymentHash)
|
||||
.then(function (response1) {
|
||||
|
||||
if (response1.data[2] == song_id) {
|
||||
setTimeout(function () { self.getCurrent() }, 500)
|
||||
self.$q.notify({
|
||||
color: 'green',
|
||||
message:
|
||||
'Success! "' + self.receive.name + '" will be played soon',
|
||||
timeout: 3000
|
||||
})
|
||||
|
||||
self.paid = false
|
||||
response1 = []
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
|
||||
LNbits.utils.notifyApiError(err)
|
||||
self.paid = false
|
||||
response1 = []
|
||||
})
|
||||
}
|
||||
}, 3000)
|
||||
|
||||
self.$q.notify({
|
||||
message: 'Processing'
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
|
||||
self.$q.notify({
|
||||
color: 'warning',
|
||||
html: true,
|
||||
|
@ -196,39 +214,17 @@
|
|||
})
|
||||
})
|
||||
},
|
||||
checkInvoice(juke_id, paymentHash) {
|
||||
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/jukebox/api/v1/jukebox/jb/checkinvoice/' + juke_id + '/' + paymentHash,
|
||||
'filla'
|
||||
)
|
||||
.then(function (response) {
|
||||
|
||||
self.paid = response.data.paid
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getCurrent() {
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/jukebox/api/v1/jukebox/jb/currently/{{juke_id}}')
|
||||
.request('GET', '/jukebox/api/v1/jukebox/jb/currently/{{juke_id}}')
|
||||
.then(function (res) {
|
||||
if (res.data.id) {
|
||||
|
||||
self.currentlyPlaying = res.data
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
|
||||
},
|
||||
selectPlaylist() {
|
||||
self = this
|
||||
|
@ -236,9 +232,9 @@
|
|||
.request(
|
||||
'GET',
|
||||
'/jukebox/api/v1/jukebox/jb/playlist/' +
|
||||
'{{ juke_id }}' +
|
||||
'/' +
|
||||
self.playlist.split(',')[0].split('-')[1]
|
||||
'{{ juke_id }}' +
|
||||
'/' +
|
||||
self.playlist.split(',')[0].split('-')[1]
|
||||
)
|
||||
.then(function (response) {
|
||||
self.currentPlaylist = response.data
|
||||
|
@ -247,20 +243,21 @@
|
|||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
currentSong() { }
|
||||
currentSong() {}
|
||||
},
|
||||
created() {
|
||||
this.getCurrent()
|
||||
this.playlists = JSON.parse('{{ playlists | tojson }}')
|
||||
|
||||
this.selectedWallet.inkey = '{{ inkey }}'
|
||||
this.startPaymentNotifier()
|
||||
self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/jukebox/api/v1/jukebox/jb/playlist/' +
|
||||
'{{ juke_id }}' +
|
||||
'/' +
|
||||
self.playlists[0].split(',')[0].split('-')[1]
|
||||
'{{ juke_id }}' +
|
||||
'/' +
|
||||
self.playlists[0].split(',')[0].split('-')[1]
|
||||
)
|
||||
.then(function (response) {
|
||||
self.currentPlaylist = response.data
|
||||
|
@ -268,9 +265,7 @@
|
|||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
|
||||
// this.startPaymentNotifier()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -9,7 +9,7 @@ from .models import Livestream, Track, Producer
|
|||
async def create_livestream(*, wallet_id: str) -> int:
|
||||
result = await db.execute(
|
||||
"""
|
||||
INSERT INTO livestreams (wallet)
|
||||
INSERT INTO livestream.livestreams (wallet)
|
||||
VALUES (?)
|
||||
""",
|
||||
(wallet_id,),
|
||||
|
@ -18,14 +18,16 @@ async def create_livestream(*, wallet_id: str) -> int:
|
|||
|
||||
|
||||
async def get_livestream(ls_id: int) -> Optional[Livestream]:
|
||||
row = await db.fetchone("SELECT * FROM livestreams WHERE id = ?", (ls_id,))
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM livestream.livestreams WHERE id = ?", (ls_id,)
|
||||
)
|
||||
return Livestream(**dict(row)) if row else None
|
||||
|
||||
|
||||
async def get_livestream_by_track(track_id: int) -> Optional[Livestream]:
|
||||
row = await db.fetchone(
|
||||
"""
|
||||
SELECT livestreams.* FROM livestreams
|
||||
SELECT livestreams.* FROM livestream.livestreams
|
||||
INNER JOIN tracks ON tracks.livestream = livestreams.id
|
||||
WHERE tracks.id = ?
|
||||
""",
|
||||
|
@ -35,7 +37,9 @@ async def get_livestream_by_track(track_id: int) -> Optional[Livestream]:
|
|||
|
||||
|
||||
async def get_or_create_livestream_by_wallet(wallet: str) -> Optional[Livestream]:
|
||||
row = await db.fetchone("SELECT * FROM livestreams WHERE wallet = ?", (wallet,))
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM livestream.livestreams WHERE wallet = ?", (wallet,)
|
||||
)
|
||||
|
||||
if not row:
|
||||
# create on the fly
|
||||
|
@ -47,14 +51,14 @@ async def get_or_create_livestream_by_wallet(wallet: str) -> Optional[Livestream
|
|||
|
||||
async def update_current_track(ls_id: int, track_id: Optional[int]):
|
||||
await db.execute(
|
||||
"UPDATE livestreams SET current_track = ? WHERE id = ?",
|
||||
"UPDATE livestream.livestreams SET current_track = ? WHERE id = ?",
|
||||
(track_id, ls_id),
|
||||
)
|
||||
|
||||
|
||||
async def update_livestream_fee(ls_id: int, fee_pct: int):
|
||||
await db.execute(
|
||||
"UPDATE livestreams SET fee_pct = ? WHERE id = ?",
|
||||
"UPDATE livestream.livestreams SET fee_pct = ? WHERE id = ?",
|
||||
(fee_pct, ls_id),
|
||||
)
|
||||
|
||||
|
@ -68,7 +72,7 @@ async def add_track(
|
|||
) -> int:
|
||||
result = await db.execute(
|
||||
"""
|
||||
INSERT INTO tracks (livestream, name, download_url, price_msat, producer)
|
||||
INSERT INTO livestream.tracks (livestream, name, download_url, price_msat, producer)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(livestream, name, download_url, price_msat, producer),
|
||||
|
@ -86,7 +90,7 @@ async def update_track(
|
|||
) -> int:
|
||||
result = await db.execute(
|
||||
"""
|
||||
UPDATE tracks SET
|
||||
UPDATE livestream.tracks SET
|
||||
name = ?,
|
||||
download_url = ?,
|
||||
price_msat = ?,
|
||||
|
@ -105,7 +109,7 @@ async def get_track(track_id: Optional[int]) -> Optional[Track]:
|
|||
row = await db.fetchone(
|
||||
"""
|
||||
SELECT id, download_url, price_msat, name, producer
|
||||
FROM tracks WHERE id = ?
|
||||
FROM livestream.tracks WHERE id = ?
|
||||
""",
|
||||
(track_id,),
|
||||
)
|
||||
|
@ -116,7 +120,7 @@ async def get_tracks(livestream: int) -> List[Track]:
|
|||
rows = await db.fetchall(
|
||||
"""
|
||||
SELECT id, download_url, price_msat, name, producer
|
||||
FROM tracks WHERE livestream = ?
|
||||
FROM livestream.tracks WHERE livestream = ?
|
||||
""",
|
||||
(livestream,),
|
||||
)
|
||||
|
@ -126,7 +130,7 @@ async def get_tracks(livestream: int) -> List[Track]:
|
|||
async def delete_track_from_livestream(livestream: int, track_id: int):
|
||||
await db.execute(
|
||||
"""
|
||||
DELETE FROM tracks WHERE livestream = ? AND id = ?
|
||||
DELETE FROM livestream.tracks WHERE livestream = ? AND id = ?
|
||||
""",
|
||||
(livestream, track_id),
|
||||
)
|
||||
|
@ -137,7 +141,7 @@ async def add_producer(livestream: int, name: str) -> int:
|
|||
|
||||
existing = await db.fetchall(
|
||||
"""
|
||||
SELECT id FROM producers
|
||||
SELECT id FROM livestream.producers
|
||||
WHERE livestream = ? AND lower(name) = ?
|
||||
""",
|
||||
(livestream, name.lower()),
|
||||
|
@ -150,7 +154,7 @@ async def add_producer(livestream: int, name: str) -> int:
|
|||
|
||||
result = await db.execute(
|
||||
"""
|
||||
INSERT INTO producers (livestream, name, user, wallet)
|
||||
INSERT INTO livestream.producers (livestream, name, "user", wallet)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(livestream, name, user.id, wallet.id),
|
||||
|
@ -161,8 +165,8 @@ async def add_producer(livestream: int, name: str) -> int:
|
|||
async def get_producer(producer_id: int) -> Optional[Producer]:
|
||||
row = await db.fetchone(
|
||||
"""
|
||||
SELECT id, user, wallet, name
|
||||
FROM producers WHERE id = ?
|
||||
SELECT id, "user", wallet, name
|
||||
FROM livestream.producers WHERE id = ?
|
||||
""",
|
||||
(producer_id,),
|
||||
)
|
||||
|
@ -172,8 +176,8 @@ async def get_producer(producer_id: int) -> Optional[Producer]:
|
|||
async def get_producers(livestream: int) -> List[Producer]:
|
||||
rows = await db.fetchall(
|
||||
"""
|
||||
SELECT id, user, wallet, name
|
||||
FROM producers WHERE livestream = ?
|
||||
SELECT id, "user", wallet, name
|
||||
FROM livestream.producers WHERE livestream = ?
|
||||
""",
|
||||
(livestream,),
|
||||
)
|
||||
|
|
|
@ -3,9 +3,9 @@ async def m001_initial(db):
|
|||
Initial livestream tables.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE livestreams (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
f"""
|
||||
CREATE TABLE livestream.livestreams (
|
||||
id {db.serial_primary_key},
|
||||
wallet TEXT NOT NULL,
|
||||
fee_pct INTEGER NOT NULL DEFAULT 10,
|
||||
current_track INTEGER
|
||||
|
@ -14,11 +14,11 @@ async def m001_initial(db):
|
|||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE producers (
|
||||
livestream INTEGER NOT NULL REFERENCES livestreams (id),
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user TEXT NOT NULL,
|
||||
f"""
|
||||
CREATE TABLE livestream.producers (
|
||||
livestream INTEGER NOT NULL REFERENCES {db.references_schema}livestreams (id),
|
||||
id {db.serial_primary_key},
|
||||
"user" TEXT NOT NULL,
|
||||
wallet TEXT NOT NULL,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
@ -26,14 +26,14 @@ async def m001_initial(db):
|
|||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE tracks (
|
||||
livestream INTEGER NOT NULL REFERENCES livestreams (id),
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
f"""
|
||||
CREATE TABLE livestream.tracks (
|
||||
livestream INTEGER NOT NULL REFERENCES {db.references_schema}livestreams (id),
|
||||
id {db.serial_primary_key},
|
||||
download_url TEXT,
|
||||
price_msat INTEGER NOT NULL DEFAULT 0,
|
||||
name TEXT,
|
||||
producer INTEGER REFERENCES producers (id) NOT NULL
|
||||
producer INTEGER REFERENCES {db.references_schema}producers (id) NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import json
|
||||
import trio # type: ignore
|
||||
import trio
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.crud import create_payment
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
<div class="col">
|
||||
{% raw %}
|
||||
<q-btn unelevated color="deep-purple" type="submit">
|
||||
<q-btn unelevated color="primary" type="submit">
|
||||
{{ nextCurrentTrack && nextCurrentTrack ===
|
||||
livestream.current_track ? 'Stop' : 'Set' }} current track
|
||||
</q-btn>
|
||||
|
@ -46,7 +46,7 @@
|
|||
></q-input>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-btn unelevated color="deep-purple" type="submit"
|
||||
<q-btn unelevated color="primary" type="submit"
|
||||
>Set percent rate</q-btn
|
||||
>
|
||||
</div>
|
||||
|
@ -61,7 +61,7 @@
|
|||
<h5 class="text-subtitle1 q-my-none">Tracks</h5>
|
||||
</div>
|
||||
<div class="col q-ml-lg">
|
||||
<q-btn unelevated color="deep-purple" @click="openAddTrackDialog"
|
||||
<q-btn unelevated color="primary" @click="openAddTrackDialog"
|
||||
>Add new track</q-btn
|
||||
>
|
||||
</div>
|
||||
|
@ -296,7 +296,7 @@
|
|||
<div class="col q-ml-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="disabledAddTrackButton()"
|
||||
type="submit"
|
||||
>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "LndHub",
|
||||
"short_description": "Access lnbits from BlueWallet or Zeus.",
|
||||
"short_description": "Access lnbits from BlueWallet or Zeus",
|
||||
"icon": "navigation",
|
||||
"contributors": ["fiatjaf"]
|
||||
}
|
||||
|
|
|
@ -10,3 +10,8 @@ lnticket_ext: Blueprint = Blueprint(
|
|||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
from .tasks import register_listeners
|
||||
|
||||
from lnbits.tasks import record_async
|
||||
|
||||
lnticket_ext.record(record_async(register_listeners))
|
||||
|
|
|
@ -18,7 +18,7 @@ async def create_ticket(
|
|||
) -> Tickets:
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO ticket (id, form, email, ltext, name, wallet, sats, paid)
|
||||
INSERT INTO lnticket.ticket (id, form, email, ltext, name, wallet, sats, paid)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(payment_hash, form, email, ltext, name, wallet, sats, False),
|
||||
|
@ -30,11 +30,13 @@ 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 lnticket.ticket WHERE id = ?", (payment_hash,)
|
||||
)
|
||||
if row[7] == False:
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE ticket
|
||||
UPDATE lnticket.ticket
|
||||
SET paid = true
|
||||
WHERE id = ?
|
||||
""",
|
||||
|
@ -47,7 +49,7 @@ async def set_ticket_paid(payment_hash: str) -> Tickets:
|
|||
amount = formdata.amountmade + row[7]
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE form
|
||||
UPDATE lnticket.form
|
||||
SET amountmade = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
|
@ -77,7 +79,7 @@ async def set_ticket_paid(payment_hash: str) -> Tickets:
|
|||
|
||||
|
||||
async def get_ticket(ticket_id: str) -> Optional[Tickets]:
|
||||
row = await db.fetchone("SELECT * FROM ticket WHERE id = ?", (ticket_id,))
|
||||
row = await db.fetchone("SELECT * FROM lnticket.ticket WHERE id = ?", (ticket_id,))
|
||||
return Tickets(**row) if row else None
|
||||
|
||||
|
||||
|
@ -87,14 +89,14 @@ 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 lnticket.ticket WHERE wallet IN ({q})", (*wallet_ids,)
|
||||
)
|
||||
|
||||
return [Tickets(**row) for row in rows]
|
||||
|
||||
|
||||
async def delete_ticket(ticket_id: str) -> None:
|
||||
await db.execute("DELETE FROM ticket WHERE id = ?", (ticket_id,))
|
||||
await db.execute("DELETE FROM lnticket.ticket WHERE id = ?", (ticket_id,))
|
||||
|
||||
|
||||
# FORMS
|
||||
|
@ -111,7 +113,7 @@ async def create_form(
|
|||
form_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO form (id, wallet, name, webhook, description, costpword, amountmade)
|
||||
INSERT INTO lnticket.form (id, wallet, name, webhook, description, costpword, amountmade)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(form_id, wallet, name, webhook, description, costpword, 0),
|
||||
|
@ -124,14 +126,16 @@ async def create_form(
|
|||
|
||||
async def update_form(form_id: str, **kwargs) -> Forms:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(f"UPDATE form SET {q} WHERE id = ?", (*kwargs.values(), form_id))
|
||||
row = await db.fetchone("SELECT * FROM form WHERE id = ?", (form_id,))
|
||||
await db.execute(
|
||||
f"UPDATE lnticket.form SET {q} WHERE id = ?", (*kwargs.values(), form_id)
|
||||
)
|
||||
row = await db.fetchone("SELECT * FROM lnticket.form WHERE id = ?", (form_id,))
|
||||
assert row, "Newly updated form couldn't be retrieved"
|
||||
return Forms(**row)
|
||||
|
||||
|
||||
async def get_form(form_id: str) -> Optional[Forms]:
|
||||
row = await db.fetchone("SELECT * FROM form WHERE id = ?", (form_id,))
|
||||
row = await db.fetchone("SELECT * FROM lnticket.form WHERE id = ?", (form_id,))
|
||||
return Forms(**row) if row else None
|
||||
|
||||
|
||||
|
@ -141,11 +145,11 @@ async def get_forms(wallet_ids: Union[str, List[str]]) -> List[Forms]:
|
|||
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM form WHERE wallet IN ({q})", (*wallet_ids,)
|
||||
f"SELECT * FROM lnticket.form WHERE wallet IN ({q})", (*wallet_ids,)
|
||||
)
|
||||
|
||||
return [Forms(**row) for row in rows]
|
||||
|
||||
|
||||
async def delete_form(form_id: str) -> None:
|
||||
await db.execute("DELETE FROM form WHERE id = ?", (form_id,))
|
||||
await db.execute("DELETE FROM lnticket.form WHERE id = ?", (form_id,))
|
||||
|
|
|
@ -2,21 +2,23 @@ async def m001_initial(db):
|
|||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS forms (
|
||||
CREATE TABLE lnticket.forms (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
costpword INTEGER NOT NULL,
|
||||
amountmade INTEGER NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
time TIMESTAMP NOT NULL DEFAULT """
|
||||
+ db.timestamp_now
|
||||
+ """
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
CREATE TABLE lnticket.tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
form TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
|
@ -24,7 +26,9 @@ async def m001_initial(db):
|
|||
name TEXT NOT NULL,
|
||||
wallet TEXT NOT NULL,
|
||||
sats INTEGER NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
time TIMESTAMP NOT NULL DEFAULT """
|
||||
+ db.timestamp_now
|
||||
+ """
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
@ -34,7 +38,7 @@ async def m002_changed(db):
|
|||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS ticket (
|
||||
CREATE TABLE lnticket.ticket (
|
||||
id TEXT PRIMARY KEY,
|
||||
form TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
|
@ -43,12 +47,16 @@ async def m002_changed(db):
|
|||
wallet TEXT NOT NULL,
|
||||
sats INTEGER 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 lnticket.tickets")
|
||||
]:
|
||||
usescsv = ""
|
||||
|
||||
for i in range(row[5]):
|
||||
|
@ -59,7 +67,7 @@ async def m002_changed(db):
|
|||
usescsv = usescsv[1:]
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO ticket (
|
||||
INSERT INTO lnticket.ticket (
|
||||
id,
|
||||
form,
|
||||
email,
|
||||
|
@ -82,14 +90,14 @@ async def m002_changed(db):
|
|||
True,
|
||||
),
|
||||
)
|
||||
await db.execute("DROP TABLE tickets")
|
||||
await db.execute("DROP TABLE lnticket.tickets")
|
||||
|
||||
|
||||
async def m003_changed(db):
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS form (
|
||||
CREATE TABLE lnticket.form (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
|
@ -97,12 +105,14 @@ async def m003_changed(db):
|
|||
description TEXT NOT NULL,
|
||||
costpword INTEGER NOT NULL,
|
||||
amountmade INTEGER 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 forms")]:
|
||||
for row in [list(row) for row in await db.fetchall("SELECT * FROM lnticket.forms")]:
|
||||
usescsv = ""
|
||||
|
||||
for i in range(row[5]):
|
||||
|
@ -113,7 +123,7 @@ async def m003_changed(db):
|
|||
usescsv = usescsv[1:]
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO form (
|
||||
INSERT INTO lnticket.form (
|
||||
id,
|
||||
wallet,
|
||||
name,
|
||||
|
@ -134,4 +144,4 @@ async def m003_changed(db):
|
|||
row[6],
|
||||
),
|
||||
)
|
||||
await db.execute("DROP TABLE forms")
|
||||
await db.execute("DROP TABLE lnticket.forms")
|
||||
|
|
37
lnbits/extensions/lnticket/tasks.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
import json
|
||||
import trio # type: ignore
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.crud import create_payment
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.tasks import register_invoice_listener, internal_invoice_paid
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .crud import get_ticket, set_ticket_paid
|
||||
|
||||
|
||||
async def register_listeners():
|
||||
invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2)
|
||||
register_invoice_listener(invoice_paid_chan_send)
|
||||
await wait_for_paid_invoices(invoice_paid_chan_recv)
|
||||
|
||||
|
||||
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
||||
async for payment in invoice_paid_chan:
|
||||
await on_invoice_paid(payment)
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if "lnticket" != payment.extra.get("tag"):
|
||||
# not a lnticket invoice
|
||||
return
|
||||
|
||||
ticket = await get_ticket(payment.checking_id)
|
||||
if not ticket:
|
||||
print("this should never happen", payment)
|
||||
return
|
||||
|
||||
await payment.set_pending(False)
|
||||
await set_ticket_paid(payment.payment_hash)
|
||||
_ticket = await get_ticket(payment.checking_id)
|
||||
print("ticket", _ticket)
|
|
@ -33,7 +33,7 @@
|
|||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="formDialog.data.name == '' || formDialog.data.text == ''"
|
||||
type="submit"
|
||||
>Submit</q-btn
|
||||
|
@ -77,7 +77,7 @@
|
|||
|
||||
{% endblock %} {% block scripts %}
|
||||
<script>
|
||||
console.log('{{ form_costpword }}')
|
||||
//console.log('{{ form_costpword }}')
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
|
@ -99,7 +99,11 @@
|
|||
show: false,
|
||||
status: 'pending',
|
||||
paymentReq: null
|
||||
}
|
||||
},
|
||||
wallet: {
|
||||
inkey: ''
|
||||
},
|
||||
cancelListener: () => {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -128,12 +132,35 @@
|
|||
},
|
||||
|
||||
closeReceiveDialog: function () {
|
||||
var checker = this.receive.paymentChecker
|
||||
var checker = this.startPaymentNotifier
|
||||
dismissMsg()
|
||||
|
||||
clearInterval(paymentChecker)
|
||||
setTimeout(function () {}, 10000)
|
||||
},
|
||||
startPaymentNotifier() {
|
||||
this.cancelListener()
|
||||
|
||||
this.cancelListener = LNbits.events.onInvoicePaid(
|
||||
this.wallet,
|
||||
payment => {
|
||||
this.receive = {
|
||||
show: false,
|
||||
status: 'complete',
|
||||
paymentReq: null
|
||||
}
|
||||
dismissMsg()
|
||||
|
||||
this.formDialog.data.name = ''
|
||||
this.formDialog.data.email = ''
|
||||
this.formDialog.data.text = ''
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Sent, thank you!',
|
||||
icon: 'thumb_up'
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
Invoice: function () {
|
||||
var self = this
|
||||
axios
|
||||
|
@ -158,39 +185,15 @@
|
|||
status: 'pending',
|
||||
paymentReq: self.paymentReq
|
||||
}
|
||||
|
||||
paymentChecker = setInterval(function () {
|
||||
axios
|
||||
.get('/lnticket/api/v1/tickets/' + self.paymentCheck)
|
||||
.then(function (res) {
|
||||
if (res.data.paid) {
|
||||
clearInterval(paymentChecker)
|
||||
self.receive = {
|
||||
show: false,
|
||||
status: 'complete',
|
||||
paymentReq: null
|
||||
}
|
||||
dismissMsg()
|
||||
|
||||
self.formDialog.data.name = ''
|
||||
self.formDialog.data.email = ''
|
||||
self.formDialog.data.text = ''
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Sent, thank you!',
|
||||
icon: 'thumb_up'
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.wallet.inkey = '{{form_wallet}}'
|
||||
this.startPaymentNotifier()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
|
||||
<q-btn unelevated color="primary" @click="formDialog.show = true"
|
||||
>New Form</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
|
@ -90,6 +90,16 @@
|
|||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Tickets</h5>
|
||||
</div>
|
||||
<!-- <div class="col-auto">
|
||||
<q-btn
|
||||
flat
|
||||
color="grey"
|
||||
icon="autorenew"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="getTickets"
|
||||
><q-tooltip> Refresh Tickets </q-tooltip></q-btn
|
||||
>
|
||||
</div> -->
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportticketsCSV"
|
||||
>Export to CSV</q-btn
|
||||
|
@ -207,7 +217,7 @@
|
|||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Update Form</q-btn
|
||||
>
|
||||
|
@ -215,7 +225,7 @@
|
|||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="formDialog.data.costpword == null || formDialog.data.costpword < 0 || formDialog.data.name == null"
|
||||
type="submit"
|
||||
>Create Form</q-btn
|
||||
|
@ -230,7 +240,7 @@
|
|||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
var mapLNTicket = function(obj) {
|
||||
const mapLNTicket = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
|
@ -243,7 +253,7 @@
|
|||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function() {
|
||||
data: function () {
|
||||
return {
|
||||
forms: [],
|
||||
tickets: [],
|
||||
|
@ -290,11 +300,12 @@
|
|||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
},
|
||||
cancelListener: () => {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getTickets: function() {
|
||||
getTickets: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
|
@ -303,40 +314,43 @@
|
|||
'/lnticket/api/v1/tickets?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function(response) {
|
||||
self.tickets = response.data.map(function(obj) {
|
||||
return mapLNTicket(obj)
|
||||
})
|
||||
.then(function (response) {
|
||||
self.tickets = response.data
|
||||
.map(function (obj) {
|
||||
if (!obj?.paid) return
|
||||
return mapLNTicket(obj)
|
||||
})
|
||||
.filter(v => v)
|
||||
})
|
||||
},
|
||||
deleteTicket: function(ticketId) {
|
||||
deleteTicket: function (ticketId) {
|
||||
var self = this
|
||||
var tickets = _.findWhere(this.tickets, {id: ticketId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this ticket')
|
||||
.onOk(function() {
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/lnticket/api/v1/tickets/' + ticketId,
|
||||
_.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey
|
||||
)
|
||||
.then(function(response) {
|
||||
self.tickets = _.reject(self.tickets, function(obj) {
|
||||
.then(function (response) {
|
||||
self.tickets = _.reject(self.tickets, function (obj) {
|
||||
return obj.id == ticketId
|
||||
})
|
||||
})
|
||||
.catch(function(error) {
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportticketsCSV: function() {
|
||||
exportticketsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
|
||||
},
|
||||
|
||||
getForms: function() {
|
||||
getForms: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
|
@ -345,16 +359,17 @@
|
|||
'/lnticket/api/v1/forms?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function(response) {
|
||||
self.forms = response.data.map(function(obj) {
|
||||
.then(function (response) {
|
||||
self.forms = response.data.map(function (obj) {
|
||||
return mapLNTicket(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
sendFormData: function() {
|
||||
sendFormData: function () {
|
||||
var wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: this.formDialog.data.wallet
|
||||
})
|
||||
this.formDialog.data.inkey = wallet.inkey
|
||||
var data = this.formDialog.data
|
||||
|
||||
if (data.id) {
|
||||
|
@ -364,22 +379,23 @@
|
|||
}
|
||||
},
|
||||
|
||||
createForm: function(wallet, data) {
|
||||
createForm: function (wallet, data) {
|
||||
var self = this
|
||||
console.log('create', data)
|
||||
LNbits.api
|
||||
.request('POST', '/lnticket/api/v1/forms', wallet.inkey, data)
|
||||
.then(function(response) {
|
||||
.then(function (response) {
|
||||
self.forms.push(mapLNTicket(response.data))
|
||||
self.formDialog.show = false
|
||||
self.formDialog.data = {}
|
||||
})
|
||||
.catch(function(error) {
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
updateformDialog: function(formId) {
|
||||
updateformDialog: function (formId) {
|
||||
var link = _.findWhere(this.forms, {id: formId})
|
||||
console.log(link.id)
|
||||
|
||||
this.formDialog.data.id = link.id
|
||||
this.formDialog.data.wallet = link.wallet
|
||||
this.formDialog.data.name = link.name
|
||||
|
@ -387,10 +403,9 @@
|
|||
this.formDialog.data.costpword = link.costpword
|
||||
this.formDialog.show = true
|
||||
},
|
||||
updateForm: function(wallet, data) {
|
||||
updateForm: function (wallet, data) {
|
||||
var self = this
|
||||
console.log(data)
|
||||
|
||||
console.log('update', data)
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
|
@ -398,50 +413,67 @@
|
|||
wallet.inkey,
|
||||
data
|
||||
)
|
||||
.then(function(response) {
|
||||
self.forms = _.reject(self.forms, function(obj) {
|
||||
.then(function (response) {
|
||||
self.forms = _.reject(self.forms, function (obj) {
|
||||
return obj.id == data.id
|
||||
})
|
||||
self.forms.push(mapLNTicket(response.data))
|
||||
self.formDialog.show = false
|
||||
self.formDialog.data = {}
|
||||
})
|
||||
.catch(function(error) {
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteForm: function(formsId) {
|
||||
deleteForm: function (formsId) {
|
||||
var self = this
|
||||
var forms = _.findWhere(this.forms, {id: formsId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this form link?')
|
||||
.onOk(function() {
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/lnticket/api/v1/forms/' + formsId,
|
||||
_.findWhere(self.g.user.wallets, {id: forms.wallet}).inkey
|
||||
)
|
||||
.then(function(response) {
|
||||
self.forms = _.reject(self.forms, function(obj) {
|
||||
.then(function (response) {
|
||||
self.forms = _.reject(self.forms, function (obj) {
|
||||
return obj.id == formsId
|
||||
})
|
||||
})
|
||||
.catch(function(error) {
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportformsCSV: function() {
|
||||
exportformsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.formsTable.columns, this.forms)
|
||||
},
|
||||
startPaymentNotifier() {
|
||||
this.cancelListener()
|
||||
|
||||
this.cancelListener = LNbits.events.onInvoicePaid(
|
||||
this.g.user.wallets[0],
|
||||
payment => {
|
||||
this.getTickets()
|
||||
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'New ticket arrived!',
|
||||
icon: 'textsms'
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
created: function() {
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getTickets()
|
||||
this.getForms()
|
||||
this.startPaymentNotifier()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from quart import g, abort, render_template
|
||||
|
||||
from lnbits.core.crud import get_wallet
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from http import HTTPStatus
|
||||
|
||||
|
@ -20,10 +21,13 @@ async def display(form_id):
|
|||
if not form:
|
||||
abort(HTTPStatus.NOT_FOUND, "LNTicket does not exist.")
|
||||
|
||||
wallet = await get_wallet(form.wallet)
|
||||
|
||||
return await render_template(
|
||||
"lnticket/display.html",
|
||||
form_id=form.id,
|
||||
form_name=form.name,
|
||||
form_desc=form.description,
|
||||
form_costpword=form.costpword,
|
||||
form_wallet=wallet.inkey,
|
||||
)
|
||||
|
|
|
@ -18,7 +18,7 @@ async def create_pay_link(
|
|||
) -> PayLink:
|
||||
result = await db.execute(
|
||||
"""
|
||||
INSERT INTO pay_links (
|
||||
INSERT INTO lnurlp.pay_links (
|
||||
wallet,
|
||||
description,
|
||||
min,
|
||||
|
@ -52,7 +52,7 @@ async def create_pay_link(
|
|||
|
||||
|
||||
async def get_pay_link(link_id: int) -> Optional[PayLink]:
|
||||
row = await db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
|
||||
row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
|
||||
return PayLink.from_row(row) if row else None
|
||||
|
||||
|
||||
|
@ -63,7 +63,7 @@ async def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
|
|||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = await db.fetchall(
|
||||
f"""
|
||||
SELECT * FROM pay_links WHERE wallet IN ({q})
|
||||
SELECT * FROM lnurlp.pay_links WHERE wallet IN ({q})
|
||||
ORDER BY Id
|
||||
""",
|
||||
(*wallet_ids,),
|
||||
|
@ -75,20 +75,20 @@ async def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
|
|||
async def update_pay_link(link_id: int, **kwargs) -> Optional[PayLink]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id)
|
||||
f"UPDATE lnurlp.pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id)
|
||||
)
|
||||
row = await db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
|
||||
row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
|
||||
return PayLink.from_row(row) if row else None
|
||||
|
||||
|
||||
async def increment_pay_link(link_id: int, **kwargs) -> Optional[PayLink]:
|
||||
q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id)
|
||||
f"UPDATE lnurlp.pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id)
|
||||
)
|
||||
row = await db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
|
||||
row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
|
||||
return PayLink.from_row(row) if row else None
|
||||
|
||||
|
||||
async def delete_pay_link(link_id: int) -> None:
|
||||
await db.execute("DELETE FROM pay_links WHERE id = ?", (link_id,))
|
||||
await db.execute("DELETE FROM lnurlp.pay_links WHERE id = ?", (link_id,))
|
||||
|
|
|
@ -3,9 +3,9 @@ async def m001_initial(db):
|
|||
Initial pay table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pay_links (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
f"""
|
||||
CREATE TABLE lnurlp.pay_links (
|
||||
id {db.serial_primary_key},
|
||||
wallet TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
|
@ -20,13 +20,13 @@ async def m002_webhooks_and_success_actions(db):
|
|||
"""
|
||||
Webhooks and success actions.
|
||||
"""
|
||||
await db.execute("ALTER TABLE pay_links ADD COLUMN webhook_url TEXT;")
|
||||
await db.execute("ALTER TABLE pay_links ADD COLUMN success_text TEXT;")
|
||||
await db.execute("ALTER TABLE pay_links ADD COLUMN success_url TEXT;")
|
||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_url TEXT;")
|
||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN success_text TEXT;")
|
||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN success_url TEXT;")
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE invoices (
|
||||
pay_link INTEGER NOT NULL REFERENCES pay_links (id),
|
||||
f"""
|
||||
CREATE TABLE lnurlp.invoices (
|
||||
pay_link INTEGER NOT NULL REFERENCES {db.references_schema}pay_links (id),
|
||||
payment_hash TEXT NOT NULL,
|
||||
webhook_sent INT, -- null means not sent, otherwise store status
|
||||
expiry INT
|
||||
|
@ -41,12 +41,12 @@ async def m003_min_max_comment_fiat(db):
|
|||
converted automatically to satoshis based on some API.
|
||||
"""
|
||||
await db.execute(
|
||||
"ALTER TABLE pay_links ADD COLUMN currency TEXT;"
|
||||
"ALTER TABLE lnurlp.pay_links ADD COLUMN currency TEXT;"
|
||||
) # null = satoshis
|
||||
await db.execute(
|
||||
"ALTER TABLE pay_links ADD COLUMN comment_chars INTEGER DEFAULT 0;"
|
||||
"ALTER TABLE lnurlp.pay_links ADD COLUMN comment_chars INTEGER DEFAULT 0;"
|
||||
)
|
||||
await db.execute("ALTER TABLE pay_links RENAME COLUMN amount TO min;")
|
||||
await db.execute("ALTER TABLE pay_links ADD COLUMN max INTEGER;")
|
||||
await db.execute("UPDATE pay_links SET max = min;")
|
||||
await db.execute("DROP TABLE invoices")
|
||||
await db.execute("ALTER TABLE lnurlp.pay_links RENAME COLUMN amount TO min;")
|
||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN max INTEGER;")
|
||||
await db.execute("UPDATE lnurlp.pay_links SET max = min;")
|
||||
await db.execute("DROP TABLE lnurlp.invoices")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import trio # type: ignore
|
||||
import trio
|
||||
import json
|
||||
import httpx
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<q-expansion-item group="api" dense expand-separator label="List pay links">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-blue">GET</span> /api/v1/links</code>
|
||||
<code><span class="text-blue">GET</span> /lnurlp/api/v1/links</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
|
@ -27,7 +27,7 @@
|
|||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span> /api/v1/links/<pay_id></code
|
||||
><span class="text-blue">GET</span> /lnurlp/api/v1/links/<pay_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
|
@ -52,11 +52,11 @@
|
|||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-green">POST</span> /api/v1/links</code>
|
||||
<code><span class="text-green">POST</span> /lnurlp/api/v1/links</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code>{"description": <string> "amount": <integer>}</code>
|
||||
<code>{"description": <string> "amount": <integer> "max": <integer> "min": <integer> "comment_chars": <integer>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
|
@ -64,7 +64,7 @@
|
|||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}api/v1/links -d '{"description":
|
||||
<string>, "amount": <integer>}' -H "Content-type:
|
||||
<string>, "amount": <integer>, "max": <integer>, "min": <integer>, "comment_chars": <integer>}' -H "Content-type:
|
||||
application/json" -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
|
@ -80,7 +80,7 @@
|
|||
<q-card-section>
|
||||
<code
|
||||
><span class="text-green">PUT</span>
|
||||
/api/v1/links/<pay_id></code
|
||||
/lnurlp/api/v1/links/<pay_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
|
@ -111,7 +111,7 @@
|
|||
<q-card-section>
|
||||
<code
|
||||
><span class="text-pink">DELETE</span>
|
||||
/api/v1/links/<pay_id></code
|
||||
/lnurlp/api/v1/links/<pay_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
|
||||
<q-btn unelevated color="primary" @click="formDialog.show = true"
|
||||
>New pay link</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
|
@ -227,14 +227,14 @@
|
|||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Update pay link</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
color="primary"
|
||||
:disable="
|
||||
formDialog.data.wallet == null ||
|
||||
formDialog.data.description == null ||
|
||||
|
|