Browse Source

Black and prettier

adminvar
benarc 4 years ago
parent
commit
533ba36a1b
  1. 88
      lnbits/core/migrations.py
  2. 2
      lnbits/core/models.py
  3. 7
      lnbits/core/services.py
  4. 93
      lnbits/core/templates/core/admin.html
  5. 4
      lnbits/core/views/api.py
  6. 11
      lnbits/core/views/generic.py

88
lnbits/core/migrations.py

@ -1,10 +1,8 @@
import sqlite3 import sqlite3
from os import getenv from os import getenv
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from .crud import ( from .crud import create_account, get_user
create_account,
get_user,
)
def m000_create_migrations_table(db): def m000_create_migrations_table(db):
db.execute( db.execute(
@ -128,7 +126,6 @@ def m002_add_fields_to_apipayments(db):
pass pass
def m003_create_admin_table(db): def m003_create_admin_table(db):
user = None user = None
site_title = None site_title = None
@ -163,7 +160,6 @@ def m003_create_admin_table(db):
if getenv("LNBITS_SERVICE_FEE"): if getenv("LNBITS_SERVICE_FEE"):
service_fee = getenv("LNBITS_SERVICE_FEE") service_fee = getenv("LNBITS_SERVICE_FEE")
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS admin ( CREATE TABLE IF NOT EXISTS admin (
@ -186,11 +182,23 @@ def m003_create_admin_table(db):
INSERT INTO admin (user, site_title, tagline, primary_color, secondary_color, allowed_users, default_wallet_name, data_folder, disabled_ext, force_https, service_fee) INSERT INTO admin (user, site_title, tagline, primary_color, secondary_color, allowed_users, default_wallet_name, data_folder, disabled_ext, force_https, service_fee)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
(user, site_title, tagline, primary_color, secondary_color, allowed_users, default_wallet_name, data_folder, disabled_ext, force_https, service_fee), (
user,
site_title,
tagline,
primary_color,
secondary_color,
allowed_users,
default_wallet_name,
data_folder,
disabled_ext,
force_https,
service_fee,
),
) )
def m003_create_funding_table(db):
def m003_create_funding_table(db):
# Make the funding table, if it does not already exist # Make the funding table, if it does not already exist
@ -220,7 +228,7 @@ def m003_create_funding_table(db):
LntxbotWallet = db.fetchall("SELECT * FROM funding WHERE backend_wallet = ?", ("LntxbotWallet",)) LntxbotWallet = db.fetchall("SELECT * FROM funding WHERE backend_wallet = ?", ("LntxbotWallet",))
OpenNodeWallet = db.fetchall("SELECT * FROM funding WHERE backend_wallet = ?", ("OpenNodeWallet",)) OpenNodeWallet = db.fetchall("SELECT * FROM funding WHERE backend_wallet = ?", ("OpenNodeWallet",))
#If the funding source rows do not exist and there is data in env for them, return the data and put it in a row # If the funding source rows do not exist and there is data in env for them, return the data and put it in a row
if getenv("CLIGHTNING_RPC") and CLightningWallet != None: if getenv("CLIGHTNING_RPC") and CLightningWallet != None:
db.execute( db.execute(
@ -228,7 +236,7 @@ def m003_create_funding_table(db):
INSERT INTO funding (id, backend_wallet, endpoint) INSERT INTO funding (id, backend_wallet, endpoint)
VALUES (?, ?, ?) VALUES (?, ?, ?)
""", """,
(urlsafe_short_hash(), "CLightningWallet",getenv("CLIGHTNING_RPC")), (urlsafe_short_hash(), "CLightningWallet", getenv("CLIGHTNING_RPC")),
) )
if getenv("LNBITS_INVOICE_MACAROON") and LnbitsWallet != None: if getenv("LNBITS_INVOICE_MACAROON") and LnbitsWallet != None:
db.execute( db.execute(
@ -236,7 +244,13 @@ def m003_create_funding_table(db):
INSERT INTO funding (id, backend_wallet, endpoint, invoice_key, admin_key) INSERT INTO funding (id, backend_wallet, endpoint, invoice_key, admin_key)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
""", """,
(urlsafe_short_hash(), "LnbitsWallet",getenv("LNBITS_ENDPOINT"),getenv("LNBITS_INVOICE_MACAROON"),getenv("LNBITS_ADMIN_MACAROON")), (
urlsafe_short_hash(),
"LnbitsWallet",
getenv("LNBITS_ENDPOINT"),
getenv("LNBITS_INVOICE_MACAROON"),
getenv("LNBITS_ADMIN_MACAROON"),
),
) )
if getenv("LND_GRPC_ENDPOINT") and LndWallet != None: if getenv("LND_GRPC_ENDPOINT") and LndWallet != None:
db.execute( db.execute(
@ -244,7 +258,16 @@ def m003_create_funding_table(db):
INSERT INTO funding (id, backend_wallet, endpoint, port, read_key, invoice_key, admin_key, cert) INSERT INTO funding (id, backend_wallet, endpoint, port, read_key, invoice_key, admin_key, cert)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", """,
(urlsafe_short_hash(), "LndWallet",getenv("LND_GRPC_ENDPOINT"),getenv("LND_GRPC_PORT"),getenv("LND_READ_MACAROON"),getenv("LND_INVOICE_MACAROON"),getenv("LND_ADMIN_MACAROON"),getenv("LND_CERT")), (
urlsafe_short_hash(),
"LndWallet",
getenv("LND_GRPC_ENDPOINT"),
getenv("LND_GRPC_PORT"),
getenv("LND_READ_MACAROON"),
getenv("LND_INVOICE_MACAROON"),
getenv("LND_ADMIN_MACAROON"),
getenv("LND_CERT"),
),
) )
if getenv("LND_REST_ENDPOINT") and LndRestWallet != None: if getenv("LND_REST_ENDPOINT") and LndRestWallet != None:
@ -253,7 +276,15 @@ def m003_create_funding_table(db):
INSERT INTO funding (id, backend_wallet, endpoint, read_key, invoice_key, admin_key, cert) INSERT INTO funding (id, backend_wallet, endpoint, read_key, invoice_key, admin_key, cert)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""", """,
(urlsafe_short_hash(), "LndRestWallet",getenv("LND_REST_ENDPOINT"),getenv("LND_REST_READ_MACAROON"),getenv("LND_REST_INVOICE_MACAROON"),getenv("LND_REST_ADMIN_MACAROON"),getenv("LND_REST_CERT")), (
urlsafe_short_hash(),
"LndRestWallet",
getenv("LND_REST_ENDPOINT"),
getenv("LND_REST_READ_MACAROON"),
getenv("LND_REST_INVOICE_MACAROON"),
getenv("LND_REST_ADMIN_MACAROON"),
getenv("LND_REST_CERT"),
),
) )
if getenv("LNPAY_INVOICE_KEY") and LNPayWallet != None: if getenv("LNPAY_INVOICE_KEY") and LNPayWallet != None:
@ -262,7 +293,15 @@ def m003_create_funding_table(db):
INSERT INTO funding (id, backend_wallet, endpoint, read_key, invoice_key, admin_key, cert) INSERT INTO funding (id, backend_wallet, endpoint, read_key, invoice_key, admin_key, cert)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""", """,
(urlsafe_short_hash(), "LNPayWallet",getenv("LNPAY_API_ENDPOINT"),getenv("LNPAY_READ_KEY"),getenv("LNPAY_INVOICE_KEY"),getenv("LNPAY_ADMIN_KEY"),getenv("LNPAY_API_KEY")), (
urlsafe_short_hash(),
"LNPayWallet",
getenv("LNPAY_API_ENDPOINT"),
getenv("LNPAY_READ_KEY"),
getenv("LNPAY_INVOICE_KEY"),
getenv("LNPAY_ADMIN_KEY"),
getenv("LNPAY_API_KEY"),
),
) )
if getenv("LNTXBOT_INVOICE_KEY") and LntxbotWallet != None: if getenv("LNTXBOT_INVOICE_KEY") and LntxbotWallet != None:
@ -271,7 +310,13 @@ def m003_create_funding_table(db):
INSERT INTO funding (id, backend_wallet, endpoint, invoice_key, admin_key) INSERT INTO funding (id, backend_wallet, endpoint, invoice_key, admin_key)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
""", """,
(urlsafe_short_hash(), "LntxbotWallet",getenv("LNTXBOT_API_ENDPOINT"),getenv("LNTXBOT_INVOICE_KEY"),getenv("LNTXBOT_ADMIN_KEY")), (
urlsafe_short_hash(),
"LntxbotWallet",
getenv("LNTXBOT_API_ENDPOINT"),
getenv("LNTXBOT_INVOICE_KEY"),
getenv("LNTXBOT_ADMIN_KEY"),
),
) )
if getenv("OPENNODE_INVOICE_KEY") and OpenNodeWallet != None: if getenv("OPENNODE_INVOICE_KEY") and OpenNodeWallet != None:
@ -280,10 +325,15 @@ def m003_create_funding_table(db):
INSERT INTO funding (id, backend_wallet, endpoint, invoice_key, admin_key) INSERT INTO funding (id, backend_wallet, endpoint, invoice_key, admin_key)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
""", """,
(urlsafe_short_hash(), "OpenNodeWallet",getenv("OPENNODE_API_ENDPOINT"),getenv("OPENNODE_INVOICE_KEY"),getenv("OPENNODE_ADMIN_KEY")), (
urlsafe_short_hash(),
"OpenNodeWallet",
getenv("OPENNODE_API_ENDPOINT"),
getenv("OPENNODE_INVOICE_KEY"),
getenv("OPENNODE_ADMIN_KEY"),
),
) )
if getenv("LNBITS_BACKEND_WALLET_CLASS"): if getenv("LNBITS_BACKEND_WALLET_CLASS"):
db.execute( db.execute(
""" """
@ -291,7 +341,5 @@ def m003_create_funding_table(db):
SET active = ? SET active = ?
WHERE backend_wallet = ? WHERE backend_wallet = ?
""", """,
( (1, getenv("LNBITS_BACKEND_WALLET_CLASS")),
1, getenv("LNBITS_BACKEND_WALLET_CLASS")
),
) )

2
lnbits/core/models.py

@ -18,6 +18,7 @@ class User(NamedTuple):
w = [wallet for wallet in self.wallets if wallet.id == wallet_id] w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
return w[0] if w else None return w[0] if w else None
class Admin(NamedTuple): class Admin(NamedTuple):
user: str user: str
site_title: str site_title: str
@ -31,6 +32,7 @@ class Admin(NamedTuple):
force_https: bool force_https: bool
service_fee: int service_fee: int
class Funding(NamedTuple): class Funding(NamedTuple):
id: str id: str
backend_wallet: str backend_wallet: str

7
lnbits/core/services.py

@ -15,12 +15,7 @@ from .crud import get_wallet, create_payment, delete_payment, check_internal, up
def create_invoice( def create_invoice(
*, *, wallet_id: str, amount: int, memo: str, description_hash: Optional[bytes] = None, extra: Optional[Dict] = None
wallet_id: str,
amount: int,
memo: str,
description_hash: Optional[bytes] = None,
extra: Optional[Dict] = None,
) -> Tuple[str, str]: ) -> Tuple[str, str]:
invoice_memo = None if description_hash else memo invoice_memo = None if description_hash else memo
storeable_memo = memo storeable_memo = memo

93
lnbits/core/templates/core/admin.html

@ -18,7 +18,11 @@ context %} {% block page %}
--> -->
<div class="q-pa-md"> <div class="q-pa-md">
<q-form @submit="LaunchLNbits" @reset="cancelAdmin" class="q-gutter-md"> <q-form
@submit="LaunchLNbits"
@reset="cancelAdmin"
class="q-gutter-md"
>
<h6 class="q-my-md">Branding</h6> <h6 class="q-my-md">Branding</h6>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
@ -124,7 +128,12 @@ context %} {% block page %}
></q-select> ></q-select>
</div> </div>
</div> </div>
<h6 class="q-my-md">Funding source information (at least one required)<small><br/>*if installed through RaspiBlitz, MyNode, etc, details should be filled in for you</small></h6> <h6 class="q-my-md">
Funding source information (at least one required)<small
><br />*if installed through RaspiBlitz, MyNode, etc, details
should be filled in for you</small
>
</h6>
<q-list bordered class="rounded-borders"> <q-list bordered class="rounded-borders">
<q-expansion-item <q-expansion-item
@ -318,7 +327,11 @@ context %} {% block page %}
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item expand-separator icon="payments" :label="data.lnpay.label"> <q-expansion-item
expand-separator
icon="payments"
:label="data.lnpay.label"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<div class="row"> <div class="row">
@ -361,7 +374,11 @@ context %} {% block page %}
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
<q-expansion-item expand-separator icon="payments" :label="data.lnbits.label"> <q-expansion-item
expand-separator
icon="payments"
:label="data.lnbits.label"
>
<q-card> <q-card>
<q-card-section> <q-card-section>
<div class="row"> <div class="row">
@ -471,9 +488,9 @@ context %} {% block page %}
</div> </div>
{% endblock %} {% block scripts %} {{ window_vars(funding) }} {% endblock %} {% block scripts %} {{ window_vars(funding) }}
<script> <script>
const queryString = window.location.search const queryString = window.location.search
const urlParams = new URLSearchParams(queryString) const urlParams = new URLSearchParams(queryString)
const usr = urlParams.get('usr') const usr = urlParams.get('usr')
new Vue({ new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
@ -513,18 +530,18 @@ const usr = urlParams.get('usr')
}, },
created: function () { created: function () {
var self = this var self = this
self.data.admin.user = '{{ admin_user }}', ;(self.data.admin.user = '{{ admin_user }}'),
self.data.admin.allowed_users ='', (self.data.admin.allowed_users = ''),
self.data.admin.site_title ='{{admin.site_title}}', (self.data.admin.site_title = '{{admin.site_title}}'),
self.data.admin.tagline = '{{admin.tagline}}', (self.data.admin.tagline = '{{admin.tagline}}'),
self.data.admin.primary_color = '{{admin.primary_color}}', (self.data.admin.primary_color = '{{admin.primary_color}}'),
self.data.admin.secondary_color = '{{admin.secondary_color}}', (self.data.admin.secondary_color = '{{admin.secondary_color}}'),
self.data.admin.service_fee = parseInt('{{admin.service_fee}}'), (self.data.admin.service_fee = parseInt('{{admin.service_fee}}')),
self.data.admin.default_wallet_name = '{{admin.default_wallet_name}}', (self.data.admin.default_wallet_name = '{{admin.default_wallet_name}}'),
self.data.admin.data_folder = '{{admin.data_folder}}', (self.data.admin.data_folder = '{{admin.data_folder}}'),
self.data.admin.disabled_ext = '{{admin.disabled_ext}}'.split(",") (self.data.admin.disabled_ext = '{{admin.disabled_ext}}'.split(','))
if (usr != null){ if (usr != null) {
self.cancel.on = true self.cancel.on = true
} }
funding = JSON.parse('{{ funding | tojson }}') funding = JSON.parse('{{ funding | tojson }}')
@ -535,73 +552,72 @@ const usr = urlParams.get('usr')
self.data.lnpay.label = 'LNpay' self.data.lnpay.label = 'LNpay'
self.data.lnbits.label = 'LNbits' self.data.lnbits.label = 'LNbits'
self.data.opennode.label = 'Opennode' self.data.opennode.label = 'Opennode'
var i; var i
for (i = 0; i < funding.length; i++) { for (i = 0; i < funding.length; i++) {
if (funding[i][1] == "CLightningWallet"){ if (funding[i][1] == 'CLightningWallet') {
self.data.clightning.endpoint = funding[i][2] self.data.clightning.endpoint = funding[i][2]
if(funding[i][8] == 1){ if (funding[i][8] == 1) {
self.data.clightning.label = 'CLightning GRPC (main funding source)' self.data.clightning.label = 'CLightning GRPC (main funding source)'
} }
} }
if (funding[i][1] == "LndRestWallet"){ if (funding[i][1] == 'LndRestWallet') {
self.data.lndrest.endpoint = funding[i][2] self.data.lndrest.endpoint = funding[i][2]
self.data.lndrest.read = funding[i][4] self.data.lndrest.read = funding[i][4]
self.data.lndrest.invoice = funding[i][5] self.data.lndrest.invoice = funding[i][5]
self.data.lndrest.admin = funding[i][6] self.data.lndrest.admin = funding[i][6]
self.data.lndrest.cert = funding[i][7] self.data.lndrest.cert = funding[i][7]
if(funding[i][8] == 1){ if (funding[i][8] == 1) {
self.data.lndrest.label = 'LND REST (main funding source)' self.data.lndrest.label = 'LND REST (main funding source)'
} }
} }
if (funding[i][1] == "LndWallet"){ if (funding[i][1] == 'LndWallet') {
self.data.lndgrpc.endpoint = funding[i][2] self.data.lndgrpc.endpoint = funding[i][2]
self.data.lndgrpc.port = funding[i][3] self.data.lndgrpc.port = funding[i][3]
self.data.lndgrpc.read = funding[i][4] self.data.lndgrpc.read = funding[i][4]
self.data.lndgrpc.invoice = funding[i][5] self.data.lndgrpc.invoice = funding[i][5]
self.data.lndgrpc.admin = funding[i][6] self.data.lndgrpc.admin = funding[i][6]
self.data.lndgrpc.cert = funding[i][7] self.data.lndgrpc.cert = funding[i][7]
if(funding[i][8] == 1){ if (funding[i][8] == 1) {
self.data.lndgrpc.label = 'LND GRPC (main funding source)' self.data.lndgrpc.label = 'LND GRPC (main funding source)'
} }
} }
if (funding[i][1] == "LntxbotWallet"){ if (funding[i][1] == 'LntxbotWallet') {
self.data.lntxbot.invoice = funding[i][5] self.data.lntxbot.invoice = funding[i][5]
self.data.lntxbot.admin = funding[i][6] self.data.lntxbot.admin = funding[i][6]
if(funding[i][8] == 1){ if (funding[i][8] == 1) {
self.data.lntxbot.label = 'LNTXBOT (main funding source)' self.data.lntxbot.label = 'LNTXBOT (main funding source)'
} }
} }
if (funding[i][1] == "LNPayWallet"){ if (funding[i][1] == 'LNPayWallet') {
self.data.lnpay.read = funding[i][4] self.data.lnpay.read = funding[i][4]
self.data.lnpay.invoice = funding[i][5] self.data.lnpay.invoice = funding[i][5]
self.data.lnpay.admin = funding[i][6] self.data.lnpay.admin = funding[i][6]
self.data.lnpay.cert = funding[i][7] self.data.lnpay.cert = funding[i][7]
if(funding[i][8] == 1){ if (funding[i][8] == 1) {
self.data.lnpay.label = 'LNpay (main funding source)' self.data.lnpay.label = 'LNpay (main funding source)'
} }
} }
if (funding[i][1] == "LnbitsWallet"){ if (funding[i][1] == 'LnbitsWallet') {
self.data.lnbits.endpoint = funding[i][2] self.data.lnbits.endpoint = funding[i][2]
self.data.lnbits.read = funding[i][4] self.data.lnbits.read = funding[i][4]
self.data.lnbits.invoice = funding[i][5] self.data.lnbits.invoice = funding[i][5]
self.data.lnbits.admin = funding[i][6] self.data.lnbits.admin = funding[i][6]
if(funding[i][8] == 1){ if (funding[i][8] == 1) {
self.data.lnbits.label = 'LNbits (main funding source)' self.data.lnbits.label = 'LNbits (main funding source)'
} }
} }
if (funding[i][1] == "OpenNodeWallet"){ if (funding[i][1] == 'OpenNodeWallet') {
self.data.opennode.read = funding[i][4] self.data.opennode.read = funding[i][4]
self.data.opennode.invoice = funding[i][5] self.data.opennode.invoice = funding[i][5]
self.data.opennode.admin = funding[i][6] self.data.opennode.admin = funding[i][6]
if(funding[i][8] == 1){ if (funding[i][8] == 1) {
self.data.opennode.label = 'Opennode (main funding source)' self.data.opennode.label = 'Opennode (main funding source)'
} }
} }
} }
}, },
methods: { methods: {
@ -614,19 +630,18 @@ const usr = urlParams.get('usr')
data.admin.disabled_ext = data.admin.disabled_ext.toString() data.admin.disabled_ext = data.admin.disabled_ext.toString()
console.log(data.admin.disabled_ext) console.log(data.admin.disabled_ext)
LNbits.api LNbits.api
.request('POST', '/api/v1/admin', "wallet.inkey", .request('POST', '/api/v1/admin', 'wallet.inkey', data.admin)
data.admin)
.then(function (response) { .then(function (response) {
console.log(response.data) console.log(response.data)
window.location.href = "/wallet?usr=" + response.data[0] window.location.href = '/wallet?usr=' + response.data[0]
}) })
.catch(function (error) { .catch(function (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
}) })
}, },
cancelAdmin: function () { cancelAdmin: function () {
if (usr != null){ if (usr != null) {
window.location.href = "/wallet?usr=" + usr window.location.href = '/wallet?usr=' + usr
} }
}, },

4
lnbits/core/views/api.py

@ -136,7 +136,6 @@ def api_update_balance():
@core_app.route("/api/v1/admin", methods=["POST"]) @core_app.route("/api/v1/admin", methods=["POST"])
@api_validate_post_request( @api_validate_post_request(
schema={ schema={
"user": {"type": "string", "empty": False, "required": True}, "user": {"type": "string", "empty": False, "required": True},
"site_title": {"type": "string", "empty": False, "required": True}, "site_title": {"type": "string", "empty": False, "required": True},
"tagline": {"type": "string", "empty": False, "required": True}, "tagline": {"type": "string", "empty": False, "required": True},
@ -147,7 +146,8 @@ def api_update_balance():
"data_folder": {"type": "string", "empty": False, "required": True}, "data_folder": {"type": "string", "empty": False, "required": True},
"disabled_ext": {"type": "string", "empty": False, "required": True}, "disabled_ext": {"type": "string", "empty": False, "required": True},
"service_fee": {"type": "integer", "min": 0, "max": 90, "required": True}, "service_fee": {"type": "integer", "min": 0, "max": 90, "required": True},
}) }
)
def api_admin(): def api_admin():
admin = get_admin(None) admin = get_admin(None)

11
lnbits/core/views/generic.py

@ -6,15 +6,7 @@ from lnbits.core import core_app
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.settings import LNBITS_ALLOWED_USERS, SERVICE_FEE, LNBITS_ADMIN_USERS from lnbits.settings import LNBITS_ALLOWED_USERS, SERVICE_FEE, LNBITS_ADMIN_USERS
from ..crud import ( from ..crud import create_account, get_user, get_admin, get_funding, update_user_extension, create_wallet, delete_wallet
create_account,
get_user,
get_admin,
get_funding,
update_user_extension,
create_wallet,
delete_wallet,
)
@core_app.route("/favicon.ico") @core_app.route("/favicon.ico")
@ -103,6 +95,7 @@ def deletewallet():
return redirect(url_for("core.home")) return redirect(url_for("core.home"))
@core_app.route("/admin") @core_app.route("/admin")
def admin_setup(): def admin_setup():
user_id = request.args.get("usr", type=str) user_id = request.args.get("usr", type=str)

Loading…
Cancel
Save