diff --git a/lnbits/extensions/amilk/README.md b/lnbits/extensions/amilk/README.md new file mode 100644 index 00000000..f3c20fb8 --- /dev/null +++ b/lnbits/extensions/amilk/README.md @@ -0,0 +1,11 @@ +

Example Extension

+

*tagline*

+This is an example extension to help you organise and build you own. + +Try to include an image + + + +

If your extension has API endpoints, include useful ones here

+ +curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "Grpc-Metadata-macaroon: YOUR_WALLET-ADMIN/INVOICE-KEY" diff --git a/lnbits/extensions/amilk/__init__.py b/lnbits/extensions/amilk/__init__.py new file mode 100644 index 00000000..175dcb48 --- /dev/null +++ b/lnbits/extensions/amilk/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint + + +amilk_ext = Blueprint("amilk", __name__, static_folder="static", template_folder="templates") + + +from .views_api import * # noqa +from .views import * # noqa diff --git a/lnbits/extensions/amilk/config.json b/lnbits/extensions/amilk/config.json new file mode 100644 index 00000000..01959207 --- /dev/null +++ b/lnbits/extensions/amilk/config.json @@ -0,0 +1,6 @@ +{ + "name": "AMilk", + "short_description": "Assistant Faucet Milker", + "icon": "room_service", + "contributors": ["eillarra"] +} diff --git a/lnbits/extensions/amilk/crud.py b/lnbits/extensions/amilk/crud.py new file mode 100644 index 00000000..270e3ae0 --- /dev/null +++ b/lnbits/extensions/amilk/crud.py @@ -0,0 +1,44 @@ +from base64 import urlsafe_b64encode +from uuid import uuid4 +from typing import List, Optional, Union + +from lnbits.db import open_ext_db + +from .models import AMilk + + +def create_amilk(*, wallet_id: str, url: str, memo: str, amount: int) -> AMilk: + with open_ext_db("amilk") as db: + amilk_id = urlsafe_b64encode(uuid4().bytes_le).decode('utf-8') + db.execute( + """ + INSERT INTO amilks (id, wallet, url, memo, amount) + VALUES (?, ?, ?, ?, ?) + """, + (amilk_id, wallet_id, url, memo, amount), + ) + + return get_amilk(amilk_id) + + +def get_amilk(amilk_id: str) -> Optional[AMilk]: + with open_ext_db("amilk") as db: + row = db.fetchone("SELECT * FROM amilks WHERE id = ?", (amilk_id,)) + + return AMilk(**row) if row else None + + +def get_amilks(wallet_ids: Union[str, List[str]]) -> List[AMilk]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + with open_ext_db("amilk") as db: + q = ",".join(["?"] * len(wallet_ids)) + rows = db.fetchall(f"SELECT * FROM amilks WHERE wallet IN ({q})", (*wallet_ids,)) + + return [AMilk(**row) for row in rows] + + +def delete_amilk(amilk_id: str) -> None: + with open_ext_db("amilk") as db: + db.execute("DELETE FROM amilks WHERE id = ?", (amilk_id,)) diff --git a/lnbits/extensions/amilk/models.py b/lnbits/extensions/amilk/models.py new file mode 100644 index 00000000..acf6033d --- /dev/null +++ b/lnbits/extensions/amilk/models.py @@ -0,0 +1,9 @@ +from typing import NamedTuple + + +class AMilk(NamedTuple): + id: str + wallet: str + lnurl: str + atime: str + amount: int diff --git a/lnbits/extensions/amilk/schema.sql b/lnbits/extensions/amilk/schema.sql new file mode 100644 index 00000000..0b67e48d --- /dev/null +++ b/lnbits/extensions/amilk/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS amilks ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + lnurl TEXT NOT NULL, + atime TEXT NOT NULL, + amount INTEGER NOT NULL +); diff --git a/lnbits/extensions/amilk/templates/amilk/_api_docs.html b/lnbits/extensions/amilk/templates/amilk/_api_docs.html new file mode 100644 index 00000000..bd2c42ae --- /dev/null +++ b/lnbits/extensions/amilk/templates/amilk/_api_docs.html @@ -0,0 +1,16 @@ + + + + +
Assistant Faucet Milker
+

Milking faucets with software, known as "assmilking", seems at first to be black-hat, although in fact there might be some unexplored use cases. An LNURL withdraw gives someone the right to pull funds, which can be done over time. An LNURL withdraw could be used outside of just faucets, to provide money streaming and repeat payments.
Paste or scan an LNURL withdraw, enter the amount for the AMilk to pull and the frequency for it to be pulled.
+ Created by, Ben Arc

+
+ + +
\ No newline at end of file diff --git a/lnbits/extensions/amilk/templates/amilk/index.html b/lnbits/extensions/amilk/templates/amilk/index.html new file mode 100644 index 00000000..65277140 --- /dev/null +++ b/lnbits/extensions/amilk/templates/amilk/index.html @@ -0,0 +1,218 @@ +{% extends "base.html" %} + +{% from "macros.jinja" import window_vars with context %} + + +{% block page %} +
+
+ + + New AMilk + + + + + +
+
+
AMilks
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+ +
+ + +
LNbits Assistant Faucet Milker Extension
+
+ + + + {% include "amilk/_api_docs.html" %} + + +
+
+ + + + + + + + + + Create amilk + Cancel + + + +
+{% endblock %} + +{% block scripts %} + {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/amilk/views.py b/lnbits/extensions/amilk/views.py new file mode 100644 index 00000000..23cd5b14 --- /dev/null +++ b/lnbits/extensions/amilk/views.py @@ -0,0 +1,21 @@ +from flask import g, abort, render_template + +from lnbits.decorators import check_user_exists, validate_uuids +from lnbits.extensions.amilk import amilk_ext +from lnbits.helpers import Status + +from .crud import get_amilk + + +@amilk_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +def index(): + return render_template("amilk/index.html", user=g.user) + + +@amilk_ext.route("/") +def wall(amilk_id): + amilk = get_amilk(amilk_id) or abort(Status.NOT_FOUND, "AMilk does not exist.") + + return render_template("amilk/wall.html", amilk=amilk) diff --git a/lnbits/extensions/amilk/views_api.py b/lnbits/extensions/amilk/views_api.py new file mode 100644 index 00000000..15a722c4 --- /dev/null +++ b/lnbits/extensions/amilk/views_api.py @@ -0,0 +1,48 @@ +from flask import g, jsonify, request + +from lnbits.core.crud import get_user +from lnbits.decorators import api_check_wallet_macaroon, api_validate_post_request +from lnbits.helpers import Status + +from lnbits.extensions.amilk import amilk_ext +from .crud import create_amilk, get_amilk, get_amilks, delete_amilk + + +@amilk_ext.route("/api/v1/amilk", methods=["GET"]) +@api_check_wallet_macaroon(key_type="invoice") +def api_amilks(): + wallet_ids = [g.wallet.id] + + if "all_wallets" in request.args: + wallet_ids = get_user(g.wallet.user).wallet_ids + + return jsonify([amilk._asdict() for amilk in get_amilks(wallet_ids)]), Status.OK + + +@amilk_ext.route("/api/v1/amilk", methods=["POST"]) +@api_check_wallet_macaroon(key_type="invoice") +@api_validate_post_request(schema={ + "url": {"type": "string", "empty": False, "required": True}, + "memo": {"type": "string", "empty": False, "required": True}, + "amount": {"type": "integer", "min": 0, "required": True}, +}) +def api_amilk_create(): + amilk = create_amilk(wallet_id=g.wallet.id, **g.data) + + return jsonify(amilk._asdict()), Status.CREATED + + +@amilk_ext.route("/api/v1/amilk/", methods=["DELETE"]) +@api_check_wallet_macaroon(key_type="invoice") +def api_amilk_delete(amilk_id): + amilk = get_amilk(amilk_id) + + if not amilk: + return jsonify({"message": "Paywall does not exist."}), Status.NOT_FOUND + + if amilk.wallet != g.wallet.id: + return jsonify({"message": "Not your amilk."}), Status.FORBIDDEN + + delete_amilk(amilk_id) + + return "", Status.NO_CONTENT