Tipjar initial

This commit is contained in:
benarc 2021-10-20 11:14:56 +01:00
parent 39eb9d0d70
commit 40f3e8b210
12 changed files with 1068 additions and 0 deletions

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,19 @@
import asyncio
from fastapi import APIRouter, FastAPI
from fastapi.staticfiles import StaticFiles
from starlette.routing import Mount
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_tipjar")
tipjar_ext: APIRouter = APIRouter(prefix="/tipjar", tags=["tipjar"])
def tipjar_renderer():
return template_renderer(["lnbits/extensions/tipjar/templates"])
from .views_api import * # noqa
from .views 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,123 @@
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(data: createTip) -> Tip:
"""Create a new Tip"""
await db.execute(
"""
INSERT INTO tipjar.Tips (
id,
wallet,
name,
message,
sats,
tipjar
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(data.id, data.wallet, data.name, data.message, data.sats, data.tipjar),
)
tip = await get_tip(data.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.from_row(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.from_row(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.from_row(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.from_row(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,19 @@
from lnbits.core.crud import get_wallet
from .crud import get_tipjar
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,92 @@
{% 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
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) {
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,47 @@
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.route("/{id}")
async def tip(request: Request, id: str = Query(None)):
"""Return the donation form for the Tipjar corresponding to id"""
tipjar = await get_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,193 @@
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 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()
@tipjar_ext.post("/api/v1/tips")
async def api_create_tip(data: createTips, dataCreateTip: createTip):
"""Take data from tip form and return satspay charge"""
sats = data.sats
message = data.get("message", "")[:144]
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)
name = data.get("name", "")[:25]
# Ensure that description string can be split reliably
name = name.replace('"', "''")
if not name:
name = "Anonymous"
description = f'"{name}": {message}'
charge = await create_charge(
amount=sats,
webhook=webhook,
description=description,
**charge_details,
)
dataCreateTip.id = charge.id
dataCreateTip.wallet = tipjar.wallet
dataCreateTip.message = message
dataCreateTip.name = name
dataCreateTip.sats = data.sats
dataCreateTip.tipjar = data.tipjar
await create_tip(dataCreateTip)
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