Merge pull request #361 from arcbtc/FastAPI

ready for testing branch
This commit is contained in:
Arc 2021-10-20 18:46:23 +01:00 committed by GitHub
commit 055e91dadc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1241 additions and 299 deletions

View File

@ -13,7 +13,7 @@ async def create_copilot(
copilot_id = urlsafe_short_hash() copilot_id = urlsafe_short_hash()
await db.execute( await db.execute(
""" """
INSERT INTO copilot.copilots ( INSERT INTO copilot.newer_copilots (
id, id,
user, user,
lnurl_toggle, lnurl_toggle,
@ -71,24 +71,26 @@ async def update_copilot(
q = ", ".join([f"{field[0]} = ?" for field in data]) q = ", ".join([f"{field[0]} = ?" for field in data])
items = [f"{field[1]}" for field in data] items = [f"{field[1]}" for field in data]
items.append(copilot_id) items.append(copilot_id)
await db.execute(f"UPDATE copilot.copilots SET {q} WHERE id = ?", (items)) await db.execute(f"UPDATE copilot.newer_copilots SET {q} WHERE id = ?", (items))
row = await db.fetchone( row = await db.fetchone(
"SELECT * FROM copilot.copilots WHERE id = ?", (copilot_id,) "SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
) )
return Copilots(**row) if row else None return Copilots(**row) if row else None
async def get_copilot(copilot_id: str) -> Copilots: async def get_copilot(copilot_id: str) -> Copilots:
row = await db.fetchone( row = await db.fetchone(
"SELECT * FROM copilot.copilots WHERE id = ?", (copilot_id,) "SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
) )
return Copilots(**row) if row else None return Copilots(**row) if row else None
async def get_copilots(user: str) -> List[Copilots]: async def get_copilots(user: str) -> List[Copilots]:
rows = await db.fetchall("SELECT * FROM copilot.copilots WHERE user = ?", (user,)) rows = await db.fetchall(
"SELECT * FROM copilot.newer_copilots WHERE user = ?", (user,)
)
return [Copilots(**row) for row in rows] return [Copilots(**row) for row in rows]
async def delete_copilot(copilot_id: str) -> None: async def delete_copilot(copilot_id: str) -> None:
await db.execute("DELETE FROM copilot.copilots WHERE id = ?", (copilot_id,)) await db.execute("DELETE FROM copilot.newer_copilots WHERE id = ?", (copilot_id,))

View File

@ -6,6 +6,48 @@ async def m001_initial(db):
await db.execute( await db.execute(
f""" f"""
CREATE TABLE copilot.copilots ( 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}
);
"""
)
async def m002_fix_data_types(db):
"""
Fix data types.
"""
if db.type != "SQLITE":
await db.execute(
"ALTER TABLE copilot.copilots ALTER COLUMN show_price TYPE TEXT;"
)
async def m003_fix_data_types(db):
await db.execute(
f"""
CREATE TABLE copilot.newer_copilots (
id TEXT NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,
"user" TEXT, "user" TEXT,
title TEXT, title TEXT,
@ -32,48 +74,6 @@ async def m001_initial(db):
""" """
) )
async def m002_fix_data_types(db):
"""
Fix data types.
"""
if db.type != "SQLITE":
await db.execute( await db.execute(
"ALTER TABLE copilot.copilots ALTER COLUMN show_price TYPE TEXT;" "INSERT INTO copilot.newer_copilots SELECT * FROM copilot.copilots"
) )
# If needed, migration for SQLite (RENAME not working properly)
#
# await db.execute(
# f"""
# CREATE TABLE copilot.new_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 TEXT,
# amount_made INTEGER,
# fullscreen_cam INTEGER,
# iframe_url TEXT,
# timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
# );
# """
# )
#
# await db.execute("INSERT INTO copilot.new_copilots SELECT * FROM copilot.copilots;")
# await db.execute("DROP TABLE IF EXISTS copilot.copilots;")
# await db.execute("ALTER TABLE copilot.new_copilots RENAME TO copilot.copilots;")

View File

@ -5,7 +5,7 @@ from fastapi.param_functions import Query
from fastapi.params import Depends from fastapi.params import Depends
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import copilot_ext from . import copilot_ext
from .crud import ( from .crud import (
@ -54,7 +54,7 @@ async def api_copilot_retrieve(
async def api_copilot_create_or_update( async def api_copilot_create_or_update(
data: CreateCopilotData, data: CreateCopilotData,
copilot_id: str = Query(None), copilot_id: str = Query(None),
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(require_admin_key),
): ):
data.user = wallet.wallet.user data.user = wallet.wallet.user
data.wallet = wallet.wallet.id data.wallet = wallet.wallet.id
@ -67,7 +67,7 @@ async def api_copilot_create_or_update(
@copilot_ext.delete("/api/v1/copilot/{copilot_id}") @copilot_ext.delete("/api/v1/copilot/{copilot_id}")
async def api_copilot_delete( async def api_copilot_delete(
copilot_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) copilot_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
): ):
copilot = await get_copilot(copilot_id) copilot = await get_copilot(copilot_id)

View File

@ -16,4 +16,3 @@ def events_renderer():
from .views import * # noqa from .views import * # noqa
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -74,6 +74,9 @@ async def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
async def delete_ticket(payment_hash: str) -> None: async def delete_ticket(payment_hash: str) -> None:
await db.execute("DELETE FROM events.ticket WHERE id = ?", (payment_hash,)) await db.execute("DELETE FROM events.ticket WHERE id = ?", (payment_hash,))
async def delete_event_tickets(event_id: str) -> None:
await db.execute("DELETE FROM events.tickets WHERE event = ?", (event_id,))
# EVENTS # EVENTS

View File

@ -380,14 +380,14 @@
methods: { methods: {
getTickets: function () { getTickets: function () {
var self = this var self = this
console.log('obj')
LNbits.api LNbits.api
.request( .request(
'GET', 'GET',
'/events/api/v1/tickets?all_wallets', '/events/api/v1/tickets?all_wallets=true',
this.g.user.wallets[0].inkey this.g.user.wallets[0].inkey
) )
.then(function (response) { .then(function (response) {
console.log(response)
self.tickets = response.data.map(function (obj) { self.tickets = response.data.map(function (obj) {
console.log(obj) console.log(obj)
return mapEvents(obj) return mapEvents(obj)

View File

@ -15,6 +15,7 @@ from .crud import (
create_event, create_event,
create_ticket, create_ticket,
delete_event, delete_event,
delete_event_tickets,
delete_ticket, delete_ticket,
get_event, get_event,
get_event_tickets, get_event_tickets,
@ -81,6 +82,7 @@ async def api_form_delete(event_id, wallet: WalletTypeInfo = Depends(get_key_typ
) )
await delete_event(event_id) await delete_event(event_id)
await delete_event_tickets(event_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT) raise HTTPException(status_code=HTTPStatus.NO_CONTENT)

View File

@ -7,11 +7,11 @@ from fastapi import Request
from fastapi.param_functions import Query from fastapi.param_functions import Query
from fastapi.params import Depends from fastapi.params import Depends
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse, JSONResponse # type: ignore from starlette.responses import HTMLResponse # type: ignore
from lnbits.core.crud import get_wallet from lnbits.core.crud import get_wallet
from lnbits.core.services import check_invoice_status, create_invoice from lnbits.core.services import check_invoice_status, create_invoice
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import jukebox_ext from . import jukebox_ext
from .crud import ( from .crud import (
@ -30,7 +30,7 @@ from .models import CreateJukeboxPayment, CreateJukeLinkData
@jukebox_ext.get("/api/v1/jukebox") @jukebox_ext.get("/api/v1/jukebox")
async def api_get_jukeboxs( async def api_get_jukeboxs(
req: Request, req: Request,
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(require_admin_key),
all_wallets: bool = Query(False), all_wallets: bool = Query(False),
): ):
wallet_user = wallet.wallet.user wallet_user = wallet.wallet.user
@ -72,7 +72,7 @@ async def api_check_credentials_callbac(
@jukebox_ext.get("/api/v1/jukebox/{juke_id}") @jukebox_ext.get("/api/v1/jukebox/{juke_id}")
async def api_check_credentials_check( async def api_check_credentials_check(
juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
): ):
print(juke_id) print(juke_id)
jukebox = await get_jukebox(juke_id) jukebox = await get_jukebox(juke_id)
@ -85,7 +85,7 @@ async def api_check_credentials_check(
async def api_create_update_jukebox( async def api_create_update_jukebox(
data: CreateJukeLinkData, data: CreateJukeLinkData,
juke_id: str = Query(None), juke_id: str = Query(None),
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(require_admin_key),
): ):
if juke_id: if juke_id:
jukebox = await update_jukebox(data, juke_id=juke_id) jukebox = await update_jukebox(data, juke_id=juke_id)
@ -95,7 +95,7 @@ async def api_create_update_jukebox(
@jukebox_ext.delete("/api/v1/jukebox/{juke_id}") @jukebox_ext.delete("/api/v1/jukebox/{juke_id}")
async def api_delete_item(juke_id=None, wallet: WalletTypeInfo = Depends(get_key_type)): async def api_delete_item(juke_id=None, wallet: WalletTypeInfo = Depends(require_admin_key)):
await delete_jukebox(juke_id) await delete_jukebox(juke_id)
try: try:
return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)] return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)]

View File

@ -1,8 +1,7 @@
import asyncio import asyncio
from fastapi import APIRouter, FastAPI from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from starlette.routing import Mount
from lnbits.db import Database from lnbits.db import Database
from lnbits.helpers import template_renderer from lnbits.helpers import template_renderer
@ -29,10 +28,10 @@ def lnurlp_renderer():
return template_renderer(["lnbits/extensions/lnurlp/templates"]) return template_renderer(["lnbits/extensions/lnurlp/templates"])
from .views_api import * # noqa
from .views import * # noqa
from .tasks import wait_for_paid_invoices
from .lnurl import * # noqa from .lnurl import * # noqa
from .tasks import wait_for_paid_invoices
from .views import * # noqa
from .views_api import * # noqa
def lnurlp_start(): def lnurlp_start():

View File

@ -1,13 +1,14 @@
import hashlib import hashlib
import math import math
from http import HTTPStatus from http import HTTPStatus
from fastapi import FastAPI, Request
from starlette.exceptions import HTTPException from fastapi import Request
from lnurl import ( from lnurl import ( # type: ignore
LnurlPayResponse,
LnurlPayActionResponse,
LnurlErrorResponse, LnurlErrorResponse,
) # type: ignore LnurlPayActionResponse,
LnurlPayResponse,
)
from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice
from lnbits.utils.exchange_rates import get_fiat_rate_satoshis from lnbits.utils.exchange_rates import get_fiat_rate_satoshis

View File

@ -1,23 +1,21 @@
from http import HTTPStatus from http import HTTPStatus
from fastapi import Request
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists from lnbits.decorators import check_user_exists
from . import lnurlp_ext, lnurlp_renderer from . import lnurlp_ext, lnurlp_renderer
from .crud import get_pay_link from .crud import get_pay_link
from fastapi import FastAPI, Request
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.models import User
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@lnurlp_ext.get("/", response_class=HTMLResponse) @lnurlp_ext.get("/", response_class=HTMLResponse)
# @validate_uuids(["usr"], required=True)
# @check_user_exists()
async def index(request: Request, user: User = Depends(check_user_exists)): async def index(request: Request, user: User = Depends(check_user_exists)):
return lnurlp_renderer().TemplateResponse( return lnurlp_renderer().TemplateResponse(
"lnurlp/index.html", {"request": request, "user": user.dict()} "lnurlp/index.html", {"request": request, "user": user.dict()}
@ -31,7 +29,6 @@ async def display(request: Request, link_id):
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist." status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
) )
# abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.")
ctx = {"request": request, "lnurl": link.lnurl(req=request)} ctx = {"request": request, "lnurl": link.lnurl(req=request)}
return lnurlp_renderer().TemplateResponse("lnurlp/display.html", ctx) return lnurlp_renderer().TemplateResponse("lnurlp/display.html", ctx)
@ -43,6 +40,5 @@ async def print_qr(request: Request, link_id):
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist." status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
) )
# abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.")
ctx = {"request": request, "lnurl": link.lnurl(req=request)} ctx = {"request": request, "lnurl": link.lnurl(req=request)}
return lnurlp_renderer().TemplateResponse("lnurlp/print_qr.html", ctx) return lnurlp_renderer().TemplateResponse("lnurlp/print_qr.html", ctx)

View File

@ -1,27 +1,24 @@
from typing import Optional
from fastapi.params import Depends
from fastapi.param_functions import Query
from pydantic.main import BaseModel
from http import HTTPStatus from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from fastapi import Request
from starlette.responses import HTMLResponse, JSONResponse # type: ignore
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.utils.exchange_rates import currencies, get_fiat_rate_satoshis from lnbits.utils.exchange_rates import currencies, get_fiat_rate_satoshis
from .models import CreatePayLinkData
from . import lnurlp_ext from . import lnurlp_ext
from .crud import ( from .crud import (
create_pay_link, create_pay_link,
delete_pay_link,
get_pay_link, get_pay_link,
get_pay_links, get_pay_links,
update_pay_link, update_pay_link,
delete_pay_link,
) )
from .models import CreatePayLinkData
@lnurlp_ext.get("/api/v1/currencies") @lnurlp_ext.get("/api/v1/currencies")

View File

@ -1,26 +1,23 @@
import hashlib
from fastapi import FastAPI, Request
from fastapi.params import Depends
from http import HTTPStatus from http import HTTPStatus
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from fastapi.params import Depends
from fastapi.param_functions import Query
from lnbits.decorators import check_user_exists, WalletTypeInfo, get_key_type
from lnbits.core.crud import get_user
from lnbits.core.models import User, Payment
from . import lnurlpos_ext
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.extensions.lnurlpos import lnurlpos_ext from lnbits.extensions.lnurlpos import lnurlpos_ext
from lnbits.utils.exchange_rates import currencies
from . import lnurlpos_ext
from .crud import ( from .crud import (
create_lnurlpos, create_lnurlpos,
update_lnurlpos, delete_lnurlpos,
get_lnurlpos, get_lnurlpos,
get_lnurlposs, get_lnurlposs,
delete_lnurlpos, update_lnurlpos,
) )
from lnbits.utils.exchange_rates import currencies
from .models import createLnurlpos from .models import createLnurlpos
@ -37,7 +34,7 @@ async def api_list_currencies_available():
async def api_lnurlpos_create_or_update( async def api_lnurlpos_create_or_update(
request: Request, request: Request,
data: createLnurlpos, data: createLnurlpos,
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(require_admin_key),
lnurlpos_id: str = Query(None), lnurlpos_id: str = Query(None),
): ):
if not lnurlpos_id: if not lnurlpos_id:
@ -79,7 +76,7 @@ async def api_lnurlpos_retrieve(
@lnurlpos_ext.delete("/api/v1/lnurlpos/{lnurlpos_id}") @lnurlpos_ext.delete("/api/v1/lnurlpos/{lnurlpos_id}")
async def api_lnurlpos_delete( async def api_lnurlpos_delete(
request: Request, request: Request,
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(require_admin_key),
lnurlpos_id: str = Query(None), lnurlpos_id: str = Query(None),
): ):
lnurlpos = await get_lnurlpos(lnurlpos_id) lnurlpos = await get_lnurlpos(lnurlpos_id)

View File

@ -1,27 +1,26 @@
import json import json
from typing import List, Optional
from fastapi.params import Depends
from pydantic.main import BaseModel
from http import HTTPStatus from http import HTTPStatus
from typing import Optional
from fastapi.params import Depends
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
from pydantic.main import BaseModel
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse, JSONResponse # type: ignore from starlette.responses import HTMLResponse # type: ignore
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.utils.exchange_rates import currencies
from lnbits.requestvars import g from lnbits.requestvars import g
from lnbits.utils.exchange_rates import currencies
from . import offlineshop_ext from . import offlineshop_ext
from .crud import ( from .crud import (
add_item,
delete_item_from_shop,
get_items,
get_or_create_shop_by_wallet, get_or_create_shop_by_wallet,
set_method, set_method,
add_item,
update_item, update_item,
get_items,
delete_item_from_shop,
) )
from .models import ShopCounter from .models import ShopCounter

View File

@ -7,7 +7,7 @@ from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import satsdice_ext from . import satsdice_ext
from .crud import ( from .crud import (
@ -67,7 +67,7 @@ async def api_link_retrieve(
status_code=HTTPStatus.FORBIDDEN, detail="Not your pay link." status_code=HTTPStatus.FORBIDDEN, detail="Not your pay link."
) )
return {**link._asdict(), **{"lnurl": link.lnurl}} return {**link.dict(), **{"lnurl": link.lnurl}}
@satsdice_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED) @satsdice_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
@ -112,7 +112,7 @@ async def api_link_delete(
status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist." status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist."
) )
if link.wallet != g.wallet.id: if link.wallet != wallet.wallet.id:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your pay link." status_code=HTTPStatus.FORBIDDEN, detail="Not your pay link."
) )
@ -125,117 +125,6 @@ async def api_link_delete(
##########LNURL withdraw ##########LNURL withdraw
@satsdice_ext.get("/api/v1/withdraws")
async def api_withdraws(
wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: str = Query(None)
):
wallet_ids = [wallet.wallet.id]
if all_wallets:
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
try:
return (
jsonify(
[
{**withdraw._asdict(), **{"lnurl": withdraw.lnurl}}
for withdraw in await get_satsdice_withdraws(wallet_ids)
]
),
HTTPStatus.OK,
)
except LnurlInvalidUrl:
raise HTTPException(
status_code=HTTPStatus.UPGRADE_REQUIRED,
detail="LNURLs need to be delivered over a publically accessible `https` domain or Tor.",
)
@satsdice_ext.get("/api/v1/withdraws/{withdraw_id}")
async def api_withdraw_retrieve(
wallet: WalletTypeInfo = Depends(get_key_type), withdraw_id: str = Query(None)
):
withdraw = await get_satsdice_withdraw(withdraw_id, 0)
if not withdraw:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="satsdice withdraw does not exist."
)
if withdraw.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your satsdice withdraw."
)
return {**withdraw._asdict(), **{"lnurl": withdraw.lnurl}}, HTTPStatus.OK
@satsdice_ext.post("/api/v1/withdraws", status_code=HTTPStatus.CREATED)
@satsdice_ext.put("/api/v1/withdraws/{withdraw_id}", status_code=HTTPStatus.OK)
async def api_withdraw_create_or_update(
data: CreateSatsDiceWithdraws,
wallet: WalletTypeInfo = Depends(get_key_type),
withdraw_id: str = Query(None),
):
if data.max_satsdiceable < data.min_satsdiceable:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="`max_satsdiceable` needs to be at least `min_satsdiceable`.",
)
usescsv = ""
for i in range(data.uses):
if data.is_unique:
usescsv += "," + str(i + 1)
else:
usescsv += "," + str(1)
usescsv = usescsv[1:]
if withdraw_id:
withdraw = await get_satsdice_withdraw(withdraw_id, 0)
if not withdraw:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="satsdice withdraw does not exist.",
)
if withdraw.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your satsdice withdraw."
)
withdraw = await update_satsdice_withdraw(
withdraw_id, **data, usescsv=usescsv, used=0
)
else:
withdraw = await create_satsdice_withdraw(
wallet_id=wallet.wallet.id, **data, usescsv=usescsv
)
return {**withdraw._asdict(), **{"lnurl": withdraw.lnurl}}
@satsdice_ext.delete("/api/v1/withdraws/{withdraw_id}")
async def api_withdraw_delete(
data: CreateSatsDiceWithdraws,
wallet: WalletTypeInfo = Depends(get_key_type),
withdraw_id: str = Query(None),
):
withdraw = await get_satsdice_withdraw(withdraw_id)
if not withdraw:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="satsdice withdraw does not exist."
)
if withdraw.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your satsdice withdraw."
)
await delete_satsdice_withdraw(withdraw_id)
return "", HTTPStatus.NO_CONTENT
@satsdice_ext.get("/api/v1/withdraws/{the_hash}/{lnurl_id}") @satsdice_ext.get("/api/v1/withdraws/{the_hash}/{lnurl_id}")
async def api_withdraw_hash_retrieve( async def api_withdraw_hash_retrieve(
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(get_key_type),

View File

@ -1,29 +1,22 @@
import hashlib
from http import HTTPStatus from http import HTTPStatus
import httpx
import httpx
from fastapi import Query from fastapi import Query
from fastapi.params import Depends from fastapi.params import Depends
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import HTMLResponse, JSONResponse # type: ignore
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.extensions.satspay import satspay_ext from lnbits.extensions.satspay import satspay_ext
from .models import CreateCharge
from .crud import ( from .crud import (
check_address_balance,
create_charge, create_charge,
update_charge, delete_charge,
get_charge, get_charge,
get_charges, get_charges,
delete_charge, update_charge,
check_address_balance,
) )
from .models import CreateCharge
#############################CHARGES########################## #############################CHARGES##########################
@ -31,7 +24,7 @@ from .crud import (
@satspay_ext.post("/api/v1/charge") @satspay_ext.post("/api/v1/charge")
@satspay_ext.put("/api/v1/charge/{charge_id}") @satspay_ext.put("/api/v1/charge/{charge_id}")
async def api_charge_create_or_update( async def api_charge_create_or_update(
data: CreateCharge, wallet: WalletTypeInfo = Depends(get_key_type), charge_id=None data: CreateCharge, wallet: WalletTypeInfo = Depends(require_admin_key), charge_id=None
): ):
if not charge_id: if not charge_id:
charge = await create_charge(user=wallet.wallet.user, data=data) charge = await create_charge(user=wallet.wallet.user, data=data)

View File

@ -0,0 +1,15 @@
<h1>Tip Jars</h1>
<h2>Accept tips in Bitcoin, with small messages attached!</h2>
The TipJar extension allows you to integrate Bitcoin Lightning (and on-chain) tips into your website or social media!
![image](https://user-images.githubusercontent.com/28876473/134997129-c2f3f13c-a65d-42ed-a9c4-8a1da569d74f.png)
<h2>How to set it up</h2>
1. Simply create a new Tip Jar with the desired details (onchain optional):
![image](https://user-images.githubusercontent.com/28876473/134996842-ec2f2783-2eef-4671-8eaf-023713865512.png)
1. Share the URL you get from this little button:
![image](https://user-images.githubusercontent.com/28876473/134996973-f8ed4632-ea2f-4b62-83f1-1e4c6b6c91fa.png)
<h3>And that's it already! Let the sats flow!</h3>

View File

@ -0,0 +1,16 @@
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_tipjar")
tipjar_ext: APIRouter = APIRouter(prefix="/tipjar", tags=["tipjar"])
def tipjar_renderer():
return template_renderer(["lnbits/extensions/tipjar/templates"])
from .views import * # noqa
from .views_api import * # noqa

View File

@ -0,0 +1,6 @@
{
"name": "Tip Jar",
"short_description": "Accept Bitcoin donations, with messages attached!",
"icon": "favorite",
"contributors": ["Fittiboy"]
}

View File

@ -0,0 +1,130 @@
from . import db
from .models import Tip, TipJar, createTip, createTipJar
from ..satspay.crud import delete_charge # type: ignore
from typing import Optional
from lnbits.db import SQLITE
async def create_tip(
id: int,
wallet: str,
message: str,
name: str,
sats: int,
tipjar: str,
) -> Tip:
"""Create a new Tip"""
await db.execute(
"""
INSERT INTO tipjar.Tips (
id,
wallet,
name,
message,
sats,
tipjar
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(id, wallet, name, message, sats, tipjar),
)
tip = await get_tip(id)
assert tip, "Newly created tip couldn't be retrieved"
return tip
async def create_tipjar(data: createTipJar) -> TipJar:
"""Create a new TipJar"""
returning = "" if db.type == SQLITE else "RETURNING ID"
method = db.execute if db.type == SQLITE else db.fetchone
result = await (method)(
f"""
INSERT INTO tipjar.TipJars (
name,
wallet,
webhook,
onchain
)
VALUES (?, ?, ?, ?)
{returning}
""",
(data.name, data.wallet, data.webhook, data.onchain),
)
if db.type == SQLITE:
tipjar_id = result._result_proxy.lastrowid
else:
tipjar_id = result[0]
tipjar = await get_tipjar(tipjar_id)
assert tipjar
return tipjar
async def get_tipjar(tipjar_id: int) -> Optional[TipJar]:
"""Return a tipjar by ID"""
row = await db.fetchone("SELECT * FROM tipjar.TipJars WHERE id = ?", (tipjar_id,))
return TipJar(**row) if row else None
async def get_tipjars(wallet_id: str) -> Optional[list]:
"""Return all TipJars belonging assigned to the wallet_id"""
rows = await db.fetchall(
"SELECT * FROM tipjar.TipJars WHERE wallet = ?", (wallet_id,)
)
return [TipJar(**row) for row in rows] if rows else None
async def delete_tipjar(tipjar_id: int) -> None:
"""Delete a TipJar and all corresponding Tips"""
await db.execute("DELETE FROM tipjar.TipJars WHERE id = ?", (tipjar_id,))
rows = await db.fetchall("SELECT * FROM tipjar.Tips WHERE tipjar = ?", (tipjar_id,))
for row in rows:
await delete_tip(row["id"])
async def get_tip(tip_id: str) -> Optional[Tip]:
"""Return a Tip"""
row = await db.fetchone("SELECT * FROM tipjar.Tips WHERE id = ?", (tip_id,))
return Tip(**row) if row else None
async def get_tips(wallet_id: str) -> Optional[list]:
"""Return all Tips assigned to wallet_id"""
rows = await db.fetchall("SELECT * FROM tipjar.Tips WHERE wallet = ?", (wallet_id,))
return [Tip(**row) for row in rows] if rows else None
async def delete_tip(tip_id: str) -> None:
"""Delete a Tip and its corresponding statspay charge"""
await db.execute("DELETE FROM tipjar.Tips WHERE id = ?", (tip_id,))
await delete_charge(tip_id)
async def update_tip(tip_id: str, **kwargs) -> Tip:
"""Update a Tip"""
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE tipjar.Tips SET {q} WHERE id = ?",
(*kwargs.values(), tip_id),
)
row = await db.fetchone("SELECT * FROM tipjar.Tips WHERE id = ?", (tip_id,))
assert row, "Newly updated tip couldn't be retrieved"
return Tip(**row)
async def update_tipjar(tipjar_id: str, **kwargs) -> TipJar:
"""Update a tipjar"""
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE tipjar.TipJars SET {q} WHERE id = ?",
(*kwargs.values(), tipjar_id),
)
row = await db.fetchone("SELECT * FROM tipjar.TipJars WHERE id = ?", (tipjar_id,))
assert row, "Newly updated tipjar couldn't be retrieved"
return TipJar(**row)

View File

@ -0,0 +1,20 @@
from lnbits.core.crud import get_wallet
from .crud import get_tipjar
import json
async def get_charge_details(tipjar_id):
"""Return the default details for a satspay charge"""
tipjar = await get_tipjar(tipjar_id)
wallet_id = tipjar.wallet
wallet = await get_wallet(wallet_id)
user = wallet.user
details = {
"time": 1440,
"user": user,
"lnbitswallet": wallet_id,
"onchainwallet": tipjar.onchain,
"completelink": "/tipjar/" + str(tipjar_id),
"completelinktext": "Thanks for the tip!",
}
return details

View File

@ -0,0 +1,27 @@
async def m001_initial(db):
await db.execute(
f"""
CREATE TABLE IF NOT EXISTS tipjar.TipJars (
id {db.serial_primary_key},
name TEXT NOT NULL,
wallet TEXT NOT NULL,
onchain TEXT,
webhook TEXT
);
"""
)
await db.execute(
f"""
CREATE TABLE IF NOT EXISTS tipjar.Tips (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
name TEXT NOT NULL,
message TEXT NOT NULL,
sats INT NOT NULL,
tipjar INT NOT NULL,
FOREIGN KEY(tipjar) REFERENCES {db.references_schema}TipJars(id)
);
"""
)

View File

@ -0,0 +1,64 @@
import json
from lnurl import Lnurl, LnurlWithdrawResponse, encode as lnurl_encode # type: ignore
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult
from lnurl.types import LnurlPayMetadata # type: ignore
from sqlite3 import Row
from typing import NamedTuple, Optional, Dict
import shortuuid # type: ignore
from fastapi.param_functions import Query
from pydantic.main import BaseModel
from pydantic import BaseModel
from typing import Optional, NamedTuple
from fastapi import FastAPI, Request
class createTip(BaseModel):
id: str
wallet: str
sats: int
tipjar: int
name: str = "Anonymous"
message: str = ""
class Tip(NamedTuple):
"""A Tip represents a single donation"""
id: str # This ID always corresponds to a satspay charge ID
wallet: str
name: str # Name of the donor
message: str # Donation message
sats: int
tipjar: int # The ID of the corresponding tip jar
@classmethod
def from_row(cls, row: Row) -> "Tip":
return cls(**dict(row))
class createTipJar(BaseModel):
name: str
wallet: str
webhook: str = None
onchain: str = None
class createTips(BaseModel):
name: str
sats: str
tipjar: str
message: str
class TipJar(NamedTuple):
"""A TipJar represents a user's tip jar"""
id: int
name: str # The name of the donatee
wallet: str # Lightning wallet
onchain: Optional[str] # Watchonly wallet
webhook: Optional[str] # URL to POST tips to
@classmethod
def from_row(cls, row: Row) -> "TipJar":
return cls(**dict(row))

View File

@ -0,0 +1,16 @@
<q-card>
<q-card-section>
<h4 class="text-subtitle1 q-my-none">
Tip Jar: Receive tips with messages!
</h4>
<p>
Your personal Bitcoin tip page, which supports
lightning and on-chain payments.
Notifications, including a donation message,
can be sent via webhook.
<small>
Created by, <a href="https://github.com/Fittiboy">Fitti</a></small
>
</p>
</q-card-section>
</q-card>

View File

@ -0,0 +1,94 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<h5 class="q-my-none">Tip {{ donatee }} some sats!</h5>
<br />
<q-form @submit="Invoice()" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="tipDialog.data.name"
maxlength="25"
type="name"
label="Your Name (or contact info, leave blank for anonymous tip)"
></q-input>
<q-input
filled
dense
v-model.number="tipDialog.data.sats"
type="number"
min="1"
max="2100000000000000"
suffix="sats"
:rules="[val => val > 0 || 'Choose a positive number of sats!']"
label="Amount of sats"
></q-input>
<q-input
filled
dense
v-model.trim="tipDialog.data.message"
maxlength="144"
type="textarea"
label="Tip Message (you can leave this blank too)"
></q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="tipDialog.data.sats < 1 || !tipDialog.data.sats"
type="submit"
>Submit</q-btn
>
</div>
</q-form>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
paymentReq: null,
redirectUrl: null,
tipDialog: {
show: false,
data: {
name: '',
sats: '',
message: ''
}
}
}
},
methods: {
Invoice: function () {
var self = this
console.log('{{ tipjar }}')
axios
.post('/tipjar/api/v1/tips', {
tipjar: '{{ tipjar }}',
name: self.tipDialog.data.name,
sats: self.tipDialog.data.sats,
message: self.tipDialog.data.message
})
.then(function (response) {
console.log(response.data)
self.redirect_url = response.data.redirect_url
console.log(self.redirect_url)
window.location.href = self.redirect_url
})
}
}
})
</script>
{% endblock %}

View File

@ -0,0 +1,447 @@
{% 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-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="primary" @click="tipjarDialog.show = true"
>New TipJar</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">TipJars</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exporttipjarsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="tipjars"
row-key="id"
:columns="tipjarsTable.columns"
:pagination.sync="tipjarsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</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="send"
:color="($q.dark.isActive) ? 'grey-8' : 'grey-6'"
type="a"
:href="props.row.displayUrl"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteTipJar(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</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">Tips</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exporttipsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="tips"
:columns="tipsTable.columns"
:pagination.sync="tipsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteTip(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} TipJar extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "tipjar/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="tipjarDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendTipJarData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="tipjarDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<div class="row">
<div class="col">
<div v-if="walletLinks.length > 0">
<q-checkbox v-model="tipjarDialog.data.chain" label="Chain" />
</div>
<div v-else>
<q-checkbox :value="false" label="Chain" disabled>
<q-tooltip>
Watch-Only extension MUST be activated and have a wallet
</q-tooltip>
</q-checkbox>
</div>
</div>
</div>
<div v-if="tipjarDialog.data.chain">
<q-select
filled
dense
emit-value
v-model="tipjarDialog.data.onchain"
:options="walletLinks"
label="Chain Wallet"
/>
</div>
<q-input
filled
dense
v-model.trim="tipjarDialog.data.name"
type="text"
label="Donatee name *"
></q-input>
<q-input
filled
dense
v-model.trim="tipjarDialog.data.webhook"
type="url"
label="Webhook (URL to send tip details to once paid)"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="tipjarDialog.data.id"
unelevated
color="primary"
type="submit"
>Update TipJar</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="tipjarDialog.data.name == null"
type="submit"
>Create TipJar</q-btn
>
<q-btn v-close-popup 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>
var mapTipJar = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.displayUrl = ['/tipjar/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tipjars: [],
tips: [],
walletLinks: [],
tipjarsTable: {
columns: [
{
name: 'id',
align: 'left',
label: 'ID',
field: 'id'
},
{
name: 'name',
align: 'left',
label: 'Donatee',
field: 'name'
},
{
name: 'wallet',
align: 'left',
label: 'Wallet',
field: 'wallet'
},
{
name: 'onchain address',
align: 'left',
label: 'Onchain Address',
field: 'onchain'
},
{
name: 'webhook',
align: 'left',
label: 'Webhook',
field: 'webhook'
}
],
pagination: {
rowsPerPage: 10
}
},
tipsTable: {
columns: [
{
name: 'tipjar',
align: 'left',
label: 'TipJar',
field: 'tipjar'
},
{name: 'id', align: 'left', label: 'Charge ID', field: 'id'},
{name: 'name', align: 'left', label: 'Donor', field: 'name'},
{
name: 'message',
align: 'left',
label: 'Message',
field: 'message'
},
{name: 'sats', align: 'left', label: 'Sats', field: 'sats'}
],
pagination: {
rowsPerPage: 10
}
},
tipjarDialog: {
show: false,
chain: false,
data: {}
}
}
},
methods: {
getWalletLinks: function () {
var self = this
LNbits.api
.request(
'GET',
'/watchonly/api/v1/wallet',
this.g.user.wallets[0].inkey
)
.then(function (response) {
for (i = 0; i < response.data.length; i++) {
self.walletLinks.push(response.data[i].id)
}
return
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getTips: function () {
var self = this
LNbits.api
.request(
'GET',
'/tipjar/api/v1/tips',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.tips = response.data.map(function (obj) {
return mapTipJar(obj)
})
})
},
deleteTip: function (tipId) {
var self = this
var tips = _.findWhere(this.tips, {id: tipId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this tip?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/tipjar/api/v1/tips/' + tipId,
_.findWhere(self.g.user.wallets, {id: tips.wallet}).inkey
)
.then(function (response) {
self.tips = _.reject(self.tips, function (obj) {
return obj.id == ticketId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exporttipsCSV: function () {
LNbits.utils.exportCSV(this.tipsTable.columns, this.tips)
},
getTipJars: function () {
var self = this
LNbits.api
.request(
'GET',
'/tipjar/api/v1/tipjars',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.tipjars = response.data.map(function (obj) {
return mapTipJar(obj)
})
})
},
sendTipJarData: function () {
var wallet = _.findWhere(this.g.user.wallets, {
id: this.tipjarDialog.data.wallet
})
var data = this.tipjarDialog.data
this.createTipJar(wallet, data)
},
createTipJar: function (wallet, data) {
var self = this
LNbits.api
.request('POST', '/tipjar/api/v1/tipjars', wallet.inkey, data)
.then(function (response) {
self.tipjars.push(mapTipJar(response.data))
self.tipjarDialog.show = false
self.tipjarDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
updatetipjarDialog: function (tipjarId) {
var link = _.findWhere(this.tipjars, {id: tipjarId})
console.log(link.id)
this.tipjarDialog.data.id = link.id
this.tipjarDialog.data.wallet = link.wallet
this.tipjarDialog.data.name = link.name
this.tipjarDialog.data.webhook = link.webhook
this.tipjarDialog.show = true
},
deleteTipJar: function (tipjarsId) {
var self = this
var tipjars = _.findWhere(this.tipjars, {id: tipjarsId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this tipjar link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/tipjar/api/v1/tipjars/' + tipjarsId,
_.findWhere(self.g.user.wallets, {id: tipjars.wallet}).inkey
)
.then(function (response) {
self.tipjars = _.reject(self.tipjars, function (obj) {
return obj.id == tipjarsId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exporttipjarsCSV: function () {
LNbits.utils.exportCSV(this.tipjarsTable.columns, this.tipjars)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getWalletLinks()
this.getTipJars()
this.getTips()
this.getServices()
}
}
})
</script>
{% endblock %}

View File

@ -0,0 +1,48 @@
from .crud import get_tipjar
from http import HTTPStatus
import httpx
from collections import defaultdict
from lnbits.decorators import check_user_exists
from functools import wraps
import hashlib
from lnbits.core.services import check_invoice_status
from lnbits.core.crud import update_payment_status, get_standalone_payment
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from fastapi.params import Depends
from fastapi.param_functions import Query
import random
from datetime import datetime
from http import HTTPStatus
from . import tipjar_ext, tipjar_renderer
from lnbits.core.models import User, Payment
templates = Jinja2Templates(directory="templates")
@tipjar_ext.get("/")
async def index(request: Request, user: User = Depends(check_user_exists)):
return tipjar_renderer().TemplateResponse(
"tipjar/index.html", {"request": request, "user": user.dict()}
)
@tipjar_ext.get("/{tipjar_id}")
async def tip(request: Request, tipjar_id: int = Query(None)):
"""Return the donation form for the Tipjar corresponding to id"""
tipjar = await get_tipjar(tipjar_id)
print(tipjar_id)
if not tipjar:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="TipJar does not exist."
)
return tipjar_renderer().TemplateResponse(
"tipjar/display.html",
{"request": request, "donatee": tipjar.name, "tipjar": tipjar.id},
)

View File

@ -0,0 +1,208 @@
from http import HTTPStatus
import json
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
from starlette.exceptions import HTTPException
from lnbits.decorators import (
WalletTypeInfo,
get_key_type,
)
from lnbits.core.crud import get_user
from . import tipjar_ext
from .helpers import get_charge_details
from .crud import (
create_tipjar,
get_tipjar,
create_tip,
get_tipjars,
get_tip,
get_tips,
update_tip,
update_tipjar,
delete_tip,
delete_tipjar,
)
from ..satspay.crud import create_charge
from .models import createTipJar, createTips, createTip
@tipjar_ext.post("/api/v1/tipjars")
async def api_create_tipjar(data: createTipJar):
"""Create a tipjar, which holds data about how/where to post tips"""
try:
tipjar = await create_tipjar(data)
except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
return tipjar.dict()
async def user_from_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
return wallet.wallet.user
@tipjar_ext.post("/api/v1/tips")
async def api_create_tip(data: createTips):
"""Take data from tip form and return satspay charge"""
sats = data.sats
message = data.message
if not message:
message = "No message"
tipjar_id = data.tipjar
tipjar = await get_tipjar(tipjar_id)
webhook = tipjar.webhook
charge_details = await get_charge_details(tipjar.id)
print(charge_details["time"])
name = data.name
# Ensure that description string can be split reliably
name = name.replace('"', "''")
if not name:
name = "Anonymous"
description = f'"{name}": {message}'
charge = await create_charge(
user=charge_details["user"],
data={
"amount": sats,
"webhook": webhook,
"description": description,
"onchainwallet": charge_details["onchainwallet"],
"lnbitswallet": charge_details["lnbitswallet"],
"completelink": charge_details["completelink"],
"completelinktext": charge_details["completelinktext"],
"time": charge_details["time"],
},
)
await create_tip(
id=charge.id,
wallet=tipjar.wallet,
message=message,
name=name,
sats=data.sats,
tipjar=data.tipjar,
)
return {"redirect_url": f"/satspay/{charge.id}"}
@tipjar_ext.get("/api/v1/tipjars")
async def api_get_tipjars(wallet: WalletTypeInfo = Depends(get_key_type)):
"""Return list of all tipjars assigned to wallet with given invoice key"""
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
tipjars = []
for wallet_id in wallet_ids:
new_tipjars = await get_tipjars(wallet_id)
tipjars += new_tipjars if new_tipjars else []
return [tipjar._asdict() for tipjar in tipjars] if tipjars else []
@tipjar_ext.get("/api/v1/tips")
async def api_get_tips(wallet: WalletTypeInfo = Depends(get_key_type)):
"""Return list of all tips assigned to wallet with given invoice key"""
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
tips = []
for wallet_id in wallet_ids:
new_tips = await get_tips(wallet_id)
tips += new_tips if new_tips else []
return [tip._asdict() for tip in tips] if tips else []
@tipjar_ext.put("/api/v1/tips/{tip_id}")
async def api_update_tip(
wallet: WalletTypeInfo = Depends(get_key_type), tip_id: str = Query(None)
):
"""Update a tip with the data given in the request"""
if tip_id:
tip = await get_tip(tip_id)
if not tip:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Tip does not exist."
)
if tip.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your tip."
)
tip = await update_tip(tip_id, **g.data)
else:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="No tip ID specified"
)
return tip.dict()
@tipjar_ext.put("/api/v1/tipjars/{tipjar_id}")
async def api_update_tipjar(
wallet: WalletTypeInfo = Depends(get_key_type), tipjar_id: str = Query(None)
):
"""Update a tipjar with the data given in the request"""
if tipjar_id:
tipjar = await get_tipjar(tipjar_id)
if not tipjar:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="TipJar does not exist."
)
if tipjar.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your tipjar."
)
tipjar = await update_tipjar(tipjar_id, **data)
else:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="No tipjar ID specified"
)
return tipjar.dict()
@tipjar_ext.delete("/api/v1/tips/{tip_id}")
async def api_delete_tip(
wallet: WalletTypeInfo = Depends(get_key_type), tip_id: str = Query(None)
):
"""Delete the tip with the given tip_id"""
tip = await get_tip(tip_id)
if not tip:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="No tip with this ID!"
)
if tip.wallet != g.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Not authorized to delete this tip!",
)
await delete_tip(tip_id)
return "", HTTPStatus.NO_CONTENT
@tipjar_ext.delete("/api/v1/tipjars/{tipjar_id}")
async def api_delete_tipjar(
wallet: WalletTypeInfo = Depends(get_key_type), tipjar_id: str = Query(None)
):
"""Delete the tipjar with the given tipjar_id"""
tipjar = await get_tipjar(tipjar_id)
if not tipjar:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="No tipjar with this ID!",
)
if tipjar.wallet != g.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Not authorized to delete this tipjar!",
)
await delete_tipjar(tipjar_id)
return "", HTTPStatus.NO_CONTENT

View File

@ -6,7 +6,7 @@ from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user, get_wallet from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import check_invoice_status, create_invoice from lnbits.core.services import check_invoice_status, create_invoice
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import tpos_ext from . import tpos_ext
from .crud import create_tpos, delete_tpos, get_tpos, get_tposs from .crud import create_tpos, delete_tpos, get_tpos, get_tposs
@ -33,7 +33,7 @@ async def api_tpos_create(
@tpos_ext.delete("/api/v1/tposs/{tpos_id}") @tpos_ext.delete("/api/v1/tposs/{tpos_id}")
async def api_tpos_delete(tpos_id: str, wallet: WalletTypeInfo = Depends(get_key_type)): async def api_tpos_delete(tpos_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)):
tpos = await get_tpos(tpos_id) tpos = await get_tpos(tpos_id)
if not tpos: if not tpos:

View File

@ -4,13 +4,20 @@ from fastapi import Query
from fastapi.params import Depends from fastapi.params import Depends
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.watchonly import watchonly_ext from lnbits.extensions.watchonly import watchonly_ext
from .crud import (create_mempool, create_watch_wallet, delete_watch_wallet, from .crud import (
get_addresses, get_fresh_address, get_mempool, create_mempool,
get_watch_wallet, get_watch_wallets, update_mempool) create_watch_wallet,
delete_watch_wallet,
get_addresses,
get_fresh_address,
get_mempool,
get_watch_wallet,
get_watch_wallets,
update_mempool,
)
from .models import CreateWallet from .models import CreateWallet
###################WALLETS############################# ###################WALLETS#############################
@ -41,7 +48,7 @@ async def api_wallet_retrieve(
@watchonly_ext.post("/api/v1/wallet") @watchonly_ext.post("/api/v1/wallet")
async def api_wallet_create_or_update( async def api_wallet_create_or_update(
data: CreateWallet, wallet_id=None, w: WalletTypeInfo = Depends(get_key_type) data: CreateWallet, wallet_id=None, w: WalletTypeInfo = Depends(require_admin_key)
): ):
try: try:
wallet = await create_watch_wallet( wallet = await create_watch_wallet(
@ -57,7 +64,7 @@ async def api_wallet_create_or_update(
@watchonly_ext.delete("/api/v1/wallet/{wallet_id}") @watchonly_ext.delete("/api/v1/wallet/{wallet_id}")
async def api_wallet_delete(wallet_id, w: WalletTypeInfo = Depends(get_key_type)): async def api_wallet_delete(wallet_id, w: WalletTypeInfo = Depends(require_admin_key)):
wallet = await get_watch_wallet(wallet_id) wallet = await get_watch_wallet(wallet_id)
if not wallet: if not wallet:
@ -105,14 +112,14 @@ async def api_get_addresses(wallet_id, w: WalletTypeInfo = Depends(get_key_type)
@watchonly_ext.put("/api/v1/mempool") @watchonly_ext.put("/api/v1/mempool")
async def api_update_mempool( async def api_update_mempool(
endpoint: str = Query(...), w: WalletTypeInfo = Depends(get_key_type) endpoint: str = Query(...), w: WalletTypeInfo = Depends(require_admin_key)
): ):
mempool = await update_mempool(endpoint, user=w.wallet.user) mempool = await update_mempool(endpoint, user=w.wallet.user)
return mempool.dict() return mempool.dict()
@watchonly_ext.get("/api/v1/mempool") @watchonly_ext.get("/api/v1/mempool")
async def api_get_mempool(w: WalletTypeInfo = Depends(get_key_type)): async def api_get_mempool(w: WalletTypeInfo = Depends(require_admin_key)):
mempool = await get_mempool(w.wallet.user) mempool = await get_mempool(w.wallet.user)
if not mempool: if not mempool:
mempool = await create_mempool(user=w.wallet.user) mempool = await create_mempool(user=w.wallet.user)

View File

@ -7,20 +7,21 @@ from starlette.exceptions import HTTPException
from starlette.requests import Request from starlette.requests import Request
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import withdraw_ext from . import withdraw_ext
from .crud import (create_withdraw_link, from .crud import (
delete_withdraw_link, get_hash_check, get_withdraw_link, create_withdraw_link,
get_withdraw_links, update_withdraw_link) delete_withdraw_link,
get_hash_check,
get_withdraw_link,
get_withdraw_links,
update_withdraw_link,
)
from .models import CreateWithdrawData from .models import CreateWithdrawData
# from fastapi import FastAPI, Query, Response
@withdraw_ext.get("/api/v1/links", status_code=HTTPStatus.OK) @withdraw_ext.get("/api/v1/links", status_code=HTTPStatus.OK)
# @api_check_wallet_key("invoice")
async def api_links( async def api_links(
req: Request, req: Request,
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(get_key_type),
@ -42,58 +43,37 @@ async def api_links(
status_code=HTTPStatus.UPGRADE_REQUIRED, status_code=HTTPStatus.UPGRADE_REQUIRED,
detail="LNURLs need to be delivered over a publically accessible `https` domain or Tor.", detail="LNURLs need to be delivered over a publically accessible `https` domain or Tor.",
) )
# response.status_code = HTTPStatus.UPGRADE_REQUIRED
# return { "message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor." }
@withdraw_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) @withdraw_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
# @api_check_wallet_key("invoice") async def api_link_retrieve(link_id, request: Request, wallet: WalletTypeInfo = Depends(get_key_type)):
async def api_link_retrieve(link_id, wallet: WalletTypeInfo = Depends(get_key_type)):
link = await get_withdraw_link(link_id, 0) link = await get_withdraw_link(link_id, 0)
if not link: if not link:
raise HTTPException( raise HTTPException(
detail="Withdraw link does not exist.", status_code=HTTPStatus.NOT_FOUND detail="Withdraw link does not exist.", status_code=HTTPStatus.NOT_FOUND
) )
# response.status_code = HTTPStatus.NOT_FOUND
# return {"message": "Withdraw link does not exist."}
if link.wallet != wallet.wallet.id: if link.wallet != wallet.wallet.id:
raise HTTPException( raise HTTPException(
detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN
) )
# response.status_code = HTTPStatus.FORBIDDEN
# return {"message": "Not your withdraw link."}
return {**link, **{"lnurl": link.lnurl(request)}} return {**link, **{"lnurl": link.lnurl(request)}}
# class CreateData(BaseModel):
# title: str = Query(...)
# min_withdrawable: int = Query(..., ge=1)
# max_withdrawable: int = Query(..., ge=1)
# uses: int = Query(..., ge=1)
# wait_time: int = Query(..., ge=1)
# is_unique: bool
@withdraw_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED) @withdraw_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
@withdraw_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) @withdraw_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
# @api_check_wallet_key("admin")
async def api_link_create_or_update( async def api_link_create_or_update(
req: Request, req: Request,
data: CreateWithdrawData, data: CreateWithdrawData,
link_id: str = None, link_id: str = None,
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(require_admin_key),
): ):
if data.max_withdrawable < data.min_withdrawable: if data.max_withdrawable < data.min_withdrawable:
raise HTTPException( raise HTTPException(
detail="`max_withdrawable` needs to be at least `min_withdrawable`.", detail="`max_withdrawable` needs to be at least `min_withdrawable`.",
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
) )
# response.status_code = HTTPStatus.BAD_REQUEST
# return {
# "message": "`max_withdrawable` needs to be at least `min_withdrawable`."
# }
usescsv = "" usescsv = ""
for i in range(data.uses): for i in range(data.uses):
@ -109,50 +89,37 @@ async def api_link_create_or_update(
raise HTTPException( raise HTTPException(
detail="Withdraw link does not exist.", status_code=HTTPStatus.NOT_FOUND detail="Withdraw link does not exist.", status_code=HTTPStatus.NOT_FOUND
) )
# response.status_code = HTTPStatus.NOT_FOUND
# return {"message": "Withdraw link does not exist."}
if link.wallet != wallet.wallet.id: if link.wallet != wallet.wallet.id:
raise HTTPException( raise HTTPException(
detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN
) )
# response.status_code = HTTPStatus.FORBIDDEN
# return {"message": "Not your withdraw link."}
link = await update_withdraw_link(link_id, data=data, usescsv=usescsv, used=0) link = await update_withdraw_link(link_id, data=data, usescsv=usescsv, used=0)
else: else:
link = await create_withdraw_link( link = await create_withdraw_link(
wallet_id=wallet.wallet.id, data=data, usescsv=usescsv wallet_id=wallet.wallet.id, data=data, usescsv=usescsv
) )
# if link_id:
# response.status_code = HTTPStatus.OK
return {**link.dict(), **{"lnurl": link.lnurl(req)}} return {**link.dict(), **{"lnurl": link.lnurl(req)}}
@withdraw_ext.delete("/api/v1/links/{link_id}") @withdraw_ext.delete("/api/v1/links/{link_id}")
# @api_check_wallet_key("admin") async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(get_key_type)):
link = await get_withdraw_link(link_id) link = await get_withdraw_link(link_id)
if not link: if not link:
raise HTTPException( raise HTTPException(
detail="Withdraw link does not exist.", status_code=HTTPStatus.NOT_FOUND detail="Withdraw link does not exist.", status_code=HTTPStatus.NOT_FOUND
) )
# response.status_code = HTTPStatus.NOT_FOUND
# return {"message": "Withdraw link does not exist."}
if link.wallet != wallet.wallet.id: if link.wallet != wallet.wallet.id:
raise HTTPException( raise HTTPException(
detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN detail="Not your withdraw link.", status_code=HTTPStatus.FORBIDDEN
) )
# response.status_code = HTTPStatus.FORBIDDEN
# return {"message": "Not your withdraw link."}
await delete_withdraw_link(link_id) await delete_withdraw_link(link_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT) raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
# return ""
@withdraw_ext.get("/api/v1/links/{the_hash}/{lnurl_id}", status_code=HTTPStatus.OK) @withdraw_ext.get("/api/v1/links/{the_hash}/{lnurl_id}", status_code=HTTPStatus.OK)
# @api_check_wallet_key("invoice")
async def api_hash_retrieve( async def api_hash_retrieve(
the_hash, lnurl_id, wallet: WalletTypeInfo = Depends(get_key_type) the_hash, lnurl_id, wallet: WalletTypeInfo = Depends(get_key_type)
): ):