Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
dni committed Feb 17, 2023
1 parent 4a4b84a commit a30942a
Show file tree
Hide file tree
Showing 13 changed files with 1,082 additions and 0 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: release github version
on:
push:
tags:
- "[0-9]+.[0-9]+"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Create GitHub Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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>
25 changes: 25 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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
6 changes: 6 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "Tip Jar",
"short_description": "Accept Bitcoin donations, with messages attached!",
"tile": "/tipjar/static/image/tipjar.png",
"contributors": ["Fittiboy"]
}
123 changes: 123 additions & 0 deletions crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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)
27 changes: 27 additions & 0 deletions migrations.py
Original file line number Diff line number Diff line change
@@ -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 {db.big_int} NOT NULL,
tipjar {db.big_int} NOT NULL,
FOREIGN KEY(tipjar) REFERENCES {db.references_schema}TipJars(id)
);
"""
)
56 changes: 56 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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 added static/image/tipjar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions templates/tipjar/_api_docs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<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>
94 changes: 94 additions & 0 deletions templates/tipjar/display.html
Original file line number Diff line number Diff line change
@@ -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 %}
Loading

0 comments on commit a30942a

Please sign in to comment.