Add files via upload

This commit is contained in:
Arc 2020-04-14 11:39:54 +01:00 committed by GitHub
parent 2f9542bee6
commit 016d07bc4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 388 additions and 0 deletions

View File

@ -0,0 +1,11 @@
<h1>Example Extension</h1>
<h2>*tagline*</h2>
This is an example extension to help you organise and build you own.
Try to include an image
<img src="https://i.imgur.com/9i4xcQB.png">
<h2>If your extension has API endpoints, include useful ones here</h2>
<code>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"</code>

View File

@ -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

View File

@ -0,0 +1,6 @@
{
"name": "AMilk",
"short_description": "Assistant Faucet Milker",
"icon": "room_service",
"contributors": ["eillarra"]
}

View File

@ -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,))

View File

@ -0,0 +1,9 @@
from typing import NamedTuple
class AMilk(NamedTuple):
id: str
wallet: str
lnurl: str
atime: str
amount: int

View File

@ -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
);

View File

@ -0,0 +1,16 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">Assistant Faucet Milker</h5>
<p>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.<br/>Paste or scan an LNURL withdraw, enter the amount for the AMilk to pull and the frequency for it to be pulled.<br/>
<small> Created by, <a href="https://github.com/benarc">Ben Arc</a></small></p>
</q-card>
</q-card-section>
</q-card-section></q-expansion-item>

View File

@ -0,0 +1,218 @@
{% 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="deep-purple" @click="amilkDialog.show = true">New AMilk</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">AMilks</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div>
<q-table dense flat
:data="amilks"
row-key="id"
:columns="amilksTable.columns"
:pagination.sync="amilksTable.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="room_service" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.wall" target="_blank"></q-btn>
<q-btn unelevated dense size="xs" icon="link" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" :href="props.row.url" 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="deleteAMilk(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">LNbits Assistant Faucet Milker Extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "amilk/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="amilkDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form class="q-gutter-md">
<q-select filled dense emit-value v-model="amilkDialog.data.wallet" :options="g.user.walletOptions" label="Wallet *">
</q-select>
<q-input filled dense
v-model.trim="amilkDialog.data.lnurl"
type="url"
label="LNURL Withdraw"></q-input>
<q-input filled dense
v-model.number="amilkDialog.data.amount"
type="number"
label="Amount *"></q-input>
<q-input filled dense
v-model.trim="amilkDialog.data.atime"
type="number"
label="Hit frequency (secs)"
placeholder="Frequency to be hit"></q-input>
<q-btn unelevated
color="deep-purple"
:disable="amilkDialog.data.amount == null || amilkDialog.data.amount < 0 || amilkDialog.data.lnurl == null"
@click="createAMilk">Create amilk</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %}
{% block scripts %}
{{ window_vars(user) }}
<script>
var mapAMilk = 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.wall = ['/amilk/', obj.id].join('');
return obj;
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
amilks: [],
amilksTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'lnurl', align: 'left', label: 'LNURL', field: 'lnurl'},
{name: 'atime', align: 'left', label: 'Freq', field: 'atime'},
{name: 'amount', align: 'left', label: 'Amount', field: 'amount'},
],
pagination: {
rowsPerPage: 10
}
},
amilkDialog: {
show: false,
data: {}
}
};
},
methods: {
getAMilks: function () {
var self = this;
LNbits.api.request(
'GET',
'/amilk/api/v1/amilks?all_wallets',
this.g.user.wallets[0].inkey
).then(function (response) {
self.amilks = response.data.map(function (obj) {
return mapAMilk(obj);
});
});
},
createAMilk: function () {
var data = {
lnurl: this.amilkDialog.data.lnurl,
time: this.amilkDialog.data.time,
amount: this.amilkDialog.data.amount
};
var self = this;
console.log(this.amilkDialog.data.wallet);
LNbits.api.request(
'POST',
'/amilk/api/v1/amilks',
_.findWhere(this.g.user.wallets, {id: this.amilkDialog.data.wallet}).inkey,
data
).then(function (response) {
self.amilks.push(mapAMilk(response.data));
self.amilkDialog.show = false;
self.amilkDialog.data = {};
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
},
deleteAMilk: function (amilkId) {
var self = this;
var amilk = _.findWhere(this.amilks, {id: amilkId});
this.$q.dialog({
message: 'Are you sure you want to delete this AMilk link?',
ok: {
flat: true,
color: 'orange'
},
cancel: {
flat: true,
color: 'grey'
}
}).onOk(function () {
LNbits.api.request(
'DELETE',
'/amilk/api/v1/amilks/' + amilkId,
_.findWhere(self.g.user.wallets, {id: amilk.wallet}).inkey
).then(function (response) {
self.amilks = _.reject(self.amilks, function (obj) { return obj.id == amilkId; });
}).catch(function (error) {
LNbits.utils.notifyApiError(error);
});
});
},
exportCSV: function () {
LNbits.utils.exportCSV(this.amilksTable.columns, this.amilks);
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getAMilks();
}
}
});
</script>
{% endblock %}

View File

@ -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("/<amilk_id>")
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)

View File

@ -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/<amilk_id>", 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