remove tipjar (#1522)
This commit is contained in:
parent
4211998959
commit
5a8db02c60
|
@ -1,15 +0,0 @@
|
|||
<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>
|
|
@ -1,25 +0,0 @@
|
|||
from fastapi import APIRouter
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
|
||||
db = Database("ext_tipjar")
|
||||
|
||||
tipjar_ext: APIRouter = APIRouter(prefix="/tipjar", tags=["tipjar"])
|
||||
|
||||
tipjar_static_files = [
|
||||
{
|
||||
"path": "/tipjar/static",
|
||||
"app": StaticFiles(directory="lnbits/extensions/tipjar/static"),
|
||||
"name": "tipjar_static",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def tipjar_renderer():
|
||||
return template_renderer(["lnbits/extensions/tipjar/templates"])
|
||||
|
||||
|
||||
from .views import * # noqa: F401,F403
|
||||
from .views_api import * # noqa: F401,F403
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "Tip Jar",
|
||||
"short_description": "Accept Bitcoin donations, with messages attached!",
|
||||
"tile": "/tipjar/static/image/tipjar.png",
|
||||
"contributors": ["Fittiboy"]
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
from lnbits.db import SQLITE
|
||||
|
||||
# todo: use the API, not direct import
|
||||
from ..satspay.crud import delete_charge # type: ignore
|
||||
from . import db
|
||||
from .models import Tip, TipJar, createTipJar
|
||||
|
||||
|
||||
async def create_tip(
|
||||
id: str, 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"""
|
||||
rows = await db.fetchall("SELECT * FROM tipjar.Tips WHERE tipjar = ?", (tipjar_id,))
|
||||
for row in rows:
|
||||
await delete_tip(row["id"])
|
||||
await db.execute("DELETE FROM tipjar.TipJars WHERE id = ?", (tipjar_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)
|
|
@ -1,27 +0,0 @@
|
|||
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 {db.big_int} NOT NULL,
|
||||
tipjar {db.big_int} NOT NULL,
|
||||
FOREIGN KEY(tipjar) REFERENCES {db.references_schema}TipJars(id)
|
||||
);
|
||||
"""
|
||||
)
|
|
@ -1,56 +0,0 @@
|
|||
from sqlite3 import Row
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class createTip(BaseModel):
|
||||
id: str
|
||||
wallet: str
|
||||
sats: int
|
||||
tipjar: int
|
||||
name: str = "Anonymous"
|
||||
message: str = ""
|
||||
|
||||
|
||||
class Tip(BaseModel):
|
||||
"""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: Optional[str]
|
||||
onchain: Optional[str]
|
||||
|
||||
|
||||
class createTips(BaseModel):
|
||||
name: str
|
||||
sats: str
|
||||
tipjar: str
|
||||
message: str
|
||||
|
||||
|
||||
class TipJar(BaseModel):
|
||||
"""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))
|
Binary file not shown.
Before Width: | Height: | Size: 20 KiB |
|
@ -1,19 +0,0 @@
|
|||
<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 class="text-secondary" href="https://github.com/Fittiboy"
|
||||
>Fitti</a
|
||||
></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/tipjar"></q-btn>
|
||||
</q-card>
|
|
@ -1,94 +0,0 @@
|
|||
{% 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 %}
|
|
@ -1,443 +0,0 @@
|
|||
{% 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 %}
|
|
@ -1,35 +0,0 @@
|
|||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Depends, Query, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
|
||||
from . import tipjar_ext, tipjar_renderer
|
||||
from .crud import get_tipjar
|
||||
|
||||
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)
|
||||
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},
|
||||
)
|
|
@ -1,220 +0,0 @@
|
|||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Depends, Query
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type
|
||||
|
||||
# todo: use the API, not direct import
|
||||
from ..satspay.crud import create_charge # type: ignore
|
||||
from ..satspay.models import CreateCharge # type: ignore
|
||||
from . import tipjar_ext
|
||||
from .crud import (
|
||||
create_tip,
|
||||
create_tipjar,
|
||||
delete_tip,
|
||||
delete_tipjar,
|
||||
get_tip,
|
||||
get_tipjar,
|
||||
get_tipjars,
|
||||
get_tips,
|
||||
update_tip,
|
||||
update_tipjar,
|
||||
)
|
||||
from .models import createTip, createTipJar, createTips
|
||||
|
||||
|
||||
@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 = int(data.sats)
|
||||
message = data.message
|
||||
if not message:
|
||||
message = "No message"
|
||||
tipjar_id = int(data.tipjar)
|
||||
tipjar = await get_tipjar(tipjar_id)
|
||||
if not tipjar:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Tipjar does not exist."
|
||||
)
|
||||
|
||||
wallet_id = tipjar.wallet
|
||||
wallet = await get_wallet(wallet_id)
|
||||
if not wallet:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Tipjar wallet does not exist."
|
||||
)
|
||||
|
||||
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=wallet.user,
|
||||
data=CreateCharge(
|
||||
amount=sats,
|
||||
webhook=tipjar.webhook or "",
|
||||
description=description,
|
||||
onchainwallet=tipjar.onchain or "",
|
||||
lnbitswallet=tipjar.wallet,
|
||||
completelink="/tipjar/" + str(tipjar_id),
|
||||
completelinktext="Thanks for the tip!",
|
||||
time=1440,
|
||||
custom_css="",
|
||||
),
|
||||
)
|
||||
|
||||
await create_tip(
|
||||
id=charge.id,
|
||||
wallet=tipjar.wallet,
|
||||
message=message,
|
||||
name=name,
|
||||
sats=int(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"""
|
||||
user = await get_user(wallet.wallet.user)
|
||||
if not user:
|
||||
return []
|
||||
tipjars = []
|
||||
for wallet_id in user.wallet_ids:
|
||||
new_tipjars = await get_tipjars(wallet_id)
|
||||
tipjars += new_tipjars if new_tipjars else []
|
||||
return [tipjar.dict() for tipjar in tipjars]
|
||||
|
||||
|
||||
@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"""
|
||||
user = await get_user(wallet.wallet.user)
|
||||
if not user:
|
||||
return []
|
||||
tips = []
|
||||
for wallet_id in user.wallet_ids:
|
||||
new_tips = await get_tips(wallet_id)
|
||||
tips += new_tips if new_tips else []
|
||||
return [tip.dict() for tip in tips]
|
||||
|
||||
|
||||
@tipjar_ext.put("/api/v1/tips/{tip_id}")
|
||||
async def api_update_tip(
|
||||
data: createTip,
|
||||
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, **data.dict())
|
||||
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(
|
||||
data: createTipJar,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
tipjar_id: int = 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(str(tipjar_id), **data.dict())
|
||||
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 != wallet.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: int = 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 != wallet.wallet.id:
|
||||
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Not authorized to delete this tipjar!",
|
||||
)
|
||||
await delete_tipjar(tipjar_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
Loading…
Reference in New Issue
Block a user