Compare commits
No commits in common. "56236d54093f12790374fc80b86727bbba5475f9" and "aa584d2cb5dd0018c1536b07e9532d9ee347f04e" have entirely different histories.
56236d5409
...
aa584d2cb5
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,2 @@
|
|||||||
.env
|
.env
|
||||||
zaply
|
zaply
|
||||||
*_templ.go
|
|
||||||
__livereload
|
|
||||||
public/css/tailwind.css
|
|
||||||
|
9
Makefile
9
Makefile
@ -1,9 +0,0 @@
|
|||||||
.PHONY: dev
|
|
||||||
|
|
||||||
dev:
|
|
||||||
bash livereload.sh
|
|
||||||
|
|
||||||
build:
|
|
||||||
templ generate
|
|
||||||
tailwindcss -i ./public/css/input.css -o ./public/css/tailwind.css
|
|
||||||
go build -o zaply main.go
|
|
@ -1,30 +0,0 @@
|
|||||||
package components
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ekzyis/zaply/lightning"
|
|
||||||
)
|
|
||||||
|
|
||||||
templ Zap(inv *lightning.Invoice) {
|
|
||||||
<div class="bg-[#212529] w-fit zap-animate-in border border-[#212529] m-3 rounded-lg" id={ inv.PaymentHash }>
|
|
||||||
<div class="flex flex-row gap-3 items-center">
|
|
||||||
<svg width="32" height="32" viewBox="0 0 200 307" fill="#fada5e" xmlns="http://www.w3.org/2000/svg" class="ps-3">
|
|
||||||
<path d="M56 0L107.606 131H90.2129H89L1.52588e-05 131L177 307L106.979 165H121H160H200L56 0Z"/>
|
|
||||||
</svg>
|
|
||||||
<div class="flex flex-col pe-3 py-1">
|
|
||||||
<div class="text-lg text-[#f0f0f0]">{ inv.Description }</div>
|
|
||||||
<div class="text-sm text-slate-300">{ fmt.Sprintf("%.8s / %s", inv.PaymentHash, humanize(inv.Msats)) }</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
func humanize(msats int64) string {
|
|
||||||
sats := msats / 1000
|
|
||||||
if sats == 1 {
|
|
||||||
return fmt.Sprintf("%d sat", sats)
|
|
||||||
} else {
|
|
||||||
return fmt.Sprintf("%d sats", sats)
|
|
||||||
}
|
|
||||||
}
|
|
2
env/env.go
vendored
2
env/env.go
vendored
@ -16,7 +16,6 @@ var (
|
|||||||
PhoenixdLimitedAccessToken string
|
PhoenixdLimitedAccessToken string
|
||||||
CommitLongSha string
|
CommitLongSha string
|
||||||
CommitShortSha string
|
CommitShortSha string
|
||||||
Env string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Load(filenames ...string) error {
|
func Load(filenames ...string) error {
|
||||||
@ -27,7 +26,6 @@ func Load(filenames ...string) error {
|
|||||||
flag.StringVar(&PublicUrl, "PUBLIC_URL", "", "Base URL")
|
flag.StringVar(&PublicUrl, "PUBLIC_URL", "", "Base URL")
|
||||||
flag.StringVar(&PhoenixdURL, "PHOENIXD_URL", "", "Phoenixd URL")
|
flag.StringVar(&PhoenixdURL, "PHOENIXD_URL", "", "Phoenixd URL")
|
||||||
flag.StringVar(&PhoenixdLimitedAccessToken, "PHOENIXD_LIMITED_ACCESS_TOKEN", "", "Phoenixd limited access token")
|
flag.StringVar(&PhoenixdLimitedAccessToken, "PHOENIXD_LIMITED_ACCESS_TOKEN", "", "Phoenixd limited access token")
|
||||||
flag.StringVar(&Env, "ENV", "development", "Build environment")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
go.mod
1
go.mod
@ -3,7 +3,6 @@ module github.com/ekzyis/zaply
|
|||||||
go 1.23.4
|
go 1.23.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/a-h/templ v0.2.793 // indirect
|
|
||||||
github.com/btcsuite/btcutil v1.0.2 // indirect
|
github.com/btcsuite/btcutil v1.0.2 // indirect
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -1,5 +1,3 @@
|
|||||||
github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY=
|
|
||||||
github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
|
|
||||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||||
|
@ -7,16 +7,14 @@ type PaymentRequest string
|
|||||||
type Lightning interface {
|
type Lightning interface {
|
||||||
CreateInvoice(msats int64, description string) (PaymentRequest, error)
|
CreateInvoice(msats int64, description string) (PaymentRequest, error)
|
||||||
GetInvoice(paymentHash string) (*Invoice, error)
|
GetInvoice(paymentHash string) (*Invoice, error)
|
||||||
|
|
||||||
IncomingPayments() chan *Invoice
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Invoice struct {
|
type Invoice struct {
|
||||||
PaymentHash string `json:"paymentHash"`
|
PaymentHash string
|
||||||
Preimage string `json:"preimage"`
|
Preimage string
|
||||||
Msats int64 `json:"msats"`
|
Msats int64
|
||||||
Description string `json:"description"`
|
Description string
|
||||||
PaymentRequest string `json:"paymentRequest"`
|
PaymentRequest string
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time
|
||||||
ConfirmedAt time.Time `json:"confirmedAt"`
|
ConfirmedAt time.Time
|
||||||
}
|
}
|
||||||
|
@ -20,14 +20,10 @@ type Phoenixd struct {
|
|||||||
accessToken string
|
accessToken string
|
||||||
limitedAccessToken string
|
limitedAccessToken string
|
||||||
webhookUrl string
|
webhookUrl string
|
||||||
|
|
||||||
paymentsChan chan *lightning.Invoice
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPhoenixd(opts ...func(*Phoenixd) *Phoenixd) *Phoenixd {
|
func NewPhoenixd(opts ...func(*Phoenixd) *Phoenixd) *Phoenixd {
|
||||||
ln := &Phoenixd{
|
ln := &Phoenixd{}
|
||||||
paymentsChan: make(chan *lightning.Invoice),
|
|
||||||
}
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(ln)
|
opt(ln)
|
||||||
}
|
}
|
||||||
@ -157,21 +153,18 @@ func (p *Phoenixd) GetInvoice(paymentHash string) (*lightning.Invoice, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Phoenixd) IncomingPayments() chan *lightning.Invoice {
|
|
||||||
return p.paymentsChan
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Phoenixd) WebhookHandler(c echo.Context) error {
|
func (p *Phoenixd) WebhookHandler(c echo.Context) error {
|
||||||
var webhook struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
AmountSat int64 `json:"amountSat"`
|
|
||||||
PaymentHash string `json:"paymentHash"`
|
|
||||||
}
|
|
||||||
if err := c.Bind(&webhook); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
var webhook struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
AmountSat int64 `json:"amountSat"`
|
||||||
|
PaymentHash string `json:"paymentHash"`
|
||||||
|
}
|
||||||
|
if err := c.Bind(&webhook); err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
inv, err := p.GetInvoice(webhook.PaymentHash)
|
inv, err := p.GetInvoice(webhook.PaymentHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger().Error(err)
|
c.Logger().Error(err)
|
||||||
@ -183,8 +176,6 @@ func (p *Phoenixd) WebhookHandler(c echo.Context) error {
|
|||||||
inv.PaymentHash, inv.Msats, inv.Description,
|
inv.PaymentHash, inv.Msats, inv.Description,
|
||||||
inv.CreatedAt.Format(time.RFC3339), inv.ConfirmedAt.Format(time.RFC3339),
|
inv.CreatedAt.Format(time.RFC3339), inv.ConfirmedAt.Format(time.RFC3339),
|
||||||
)
|
)
|
||||||
|
|
||||||
p.paymentsChan <- inv
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return c.NoContent(http.StatusOK)
|
return c.NoContent(http.StatusOK)
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
PID=$(pidof zaply)
|
|
||||||
DIRS="components/ env/ lightning/ lnurl/ pages/ server/"
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo ":: remote port forwarding for zap-dev.ekzy.is ::"
|
|
||||||
ssh -fnNR 5555:localhost:4444 zap-dev.ekzy.is
|
|
||||||
echo
|
|
||||||
|
|
||||||
function restart_server() {
|
|
||||||
set +e
|
|
||||||
[[ -z "$PID" ]] || kill -15 $PID
|
|
||||||
ENV=development make build -B
|
|
||||||
set -e
|
|
||||||
./zaply 2>&1 &
|
|
||||||
PID=$(pidof zaply)
|
|
||||||
}
|
|
||||||
|
|
||||||
function restart() {
|
|
||||||
restart_server
|
|
||||||
# give server time start listening for connections
|
|
||||||
sleep 1
|
|
||||||
date +%s.%N > public/__livereload
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanup() {
|
|
||||||
rm -f public/__livereload
|
|
||||||
[[ -z "$PID" ]] || kill -15 $PID
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
restart
|
|
||||||
|
|
||||||
while inotifywait -r -e modify $DIRS; do
|
|
||||||
restart
|
|
||||||
done
|
|
@ -1,30 +0,0 @@
|
|||||||
package pages
|
|
||||||
|
|
||||||
templ Overlay() {
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>zaply</title>
|
|
||||||
<link href="/css/tailwind.css" rel="stylesheet">
|
|
||||||
<link href="/css/zap.css" rel="stylesheet">
|
|
||||||
<script src={ GetBaseUrl(ctx) + "/js/htmx.min.js" } integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
|
|
||||||
<script src={ GetBaseUrl(ctx) + "/js/htmx-sse.js" } crossorigin="anonymous"></script>
|
|
||||||
if GetEnv(ctx) == "development" {
|
|
||||||
<script src={ GetBaseUrl(ctx) + "/js/livereload.js" }></script>
|
|
||||||
}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div hx-ext="sse" sse-connect="/overlay/sse" sse-swap="zap" hx-swap="beforeend" class="fixed bottom-0 right-0" />
|
|
||||||
<script>
|
|
||||||
document.body.addEventListener('htmx:sseMessage', function (e) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const div = document.getElementById(e.detail.lastEventId)
|
|
||||||
div.classList.add('zap-animate-out')
|
|
||||||
div.addEventListener('animationend', div.remove)
|
|
||||||
}, 60_000)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
package pages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
|
||||||
"github.com/ekzyis/zaply/env"
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
var baseUrlContextKey = "baseUrl"
|
|
||||||
var envContextKey = "env"
|
|
||||||
|
|
||||||
func GetBaseUrl(ctx context.Context) string {
|
|
||||||
if u, ok := ctx.Value(baseUrlContextKey).(string); ok {
|
|
||||||
return strings.TrimRight(u, "/")
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetEnv(ctx context.Context) string {
|
|
||||||
if u, ok := ctx.Value(envContextKey).(string); ok {
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
return "development"
|
|
||||||
}
|
|
||||||
|
|
||||||
func OverlayHandler(c echo.Context) error {
|
|
||||||
return render(c, http.StatusOK, Overlay())
|
|
||||||
}
|
|
||||||
|
|
||||||
func render(ctx echo.Context, statusCode int, t templ.Component) error {
|
|
||||||
buf := templ.GetBuffer()
|
|
||||||
defer templ.ReleaseBuffer(buf)
|
|
||||||
|
|
||||||
renderContext := context.WithValue(ctx.Request().Context(), baseUrlContextKey, env.PublicUrl)
|
|
||||||
renderContext = context.WithValue(renderContext, envContextKey, env.Env)
|
|
||||||
|
|
||||||
if err := t.Render(renderContext, buf); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.HTML(statusCode, buf.String())
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
@ -1,26 +0,0 @@
|
|||||||
|
|
||||||
.zap-animate-in {
|
|
||||||
animation: slide-in 0.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zap-animate-out {
|
|
||||||
animation: fade-out 0.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide-in {
|
|
||||||
0% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(0%)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-out {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,290 +0,0 @@
|
|||||||
/*
|
|
||||||
Server Sent Events Extension
|
|
||||||
============================
|
|
||||||
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
/** @type {import("../htmx").HtmxInternalApi} */
|
|
||||||
var api
|
|
||||||
|
|
||||||
htmx.defineExtension('sse', {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init saves the provided reference to the internal HTMX API.
|
|
||||||
*
|
|
||||||
* @param {import("../htmx").HtmxInternalApi} api
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
init: function(apiRef) {
|
|
||||||
// store a reference to the internal API.
|
|
||||||
api = apiRef
|
|
||||||
|
|
||||||
// set a function in the public API for creating new EventSource objects
|
|
||||||
if (htmx.createEventSource == undefined) {
|
|
||||||
htmx.createEventSource = createEventSource
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getSelectors: function() {
|
|
||||||
return ['[sse-connect]', '[data-sse-connect]', '[sse-swap]', '[data-sse-swap]']
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onEvent handles all events passed to this extension.
|
|
||||||
*
|
|
||||||
* @param {string} name
|
|
||||||
* @param {Event} evt
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
onEvent: function(name, evt) {
|
|
||||||
var parent = evt.target || evt.detail.elt
|
|
||||||
switch (name) {
|
|
||||||
case 'htmx:beforeCleanupElement':
|
|
||||||
var internalData = api.getInternalData(parent)
|
|
||||||
// Try to remove remove an EventSource when elements are removed
|
|
||||||
var source = internalData.sseEventSource
|
|
||||||
if (source) {
|
|
||||||
api.triggerEvent(parent, 'htmx:sseClose', {
|
|
||||||
source,
|
|
||||||
type: 'nodeReplaced',
|
|
||||||
})
|
|
||||||
internalData.sseEventSource.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
// Try to create EventSources when elements are processed
|
|
||||||
case 'htmx:afterProcessNode':
|
|
||||||
ensureEventSourceOnElement(parent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/// ////////////////////////////////////////////
|
|
||||||
// HELPER FUNCTIONS
|
|
||||||
/// ////////////////////////////////////////////
|
|
||||||
|
|
||||||
/**
|
|
||||||
* createEventSource is the default method for creating new EventSource objects.
|
|
||||||
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
|
|
||||||
*
|
|
||||||
* @param {string} url
|
|
||||||
* @returns EventSource
|
|
||||||
*/
|
|
||||||
function createEventSource(url) {
|
|
||||||
return new EventSource(url, { withCredentials: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* registerSSE looks for attributes that can contain sse events, right
|
|
||||||
* now hx-trigger and sse-swap and adds listeners based on these attributes too
|
|
||||||
* the closest event source
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} elt
|
|
||||||
*/
|
|
||||||
function registerSSE(elt) {
|
|
||||||
// Add message handlers for every `sse-swap` attribute
|
|
||||||
if (api.getAttributeValue(elt, 'sse-swap')) {
|
|
||||||
// Find closest existing event source
|
|
||||||
var sourceElement = api.getClosestMatch(elt, hasEventSource)
|
|
||||||
if (sourceElement == null) {
|
|
||||||
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
|
|
||||||
return null // no eventsource in parentage, orphaned element
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set internalData and source
|
|
||||||
var internalData = api.getInternalData(sourceElement)
|
|
||||||
var source = internalData.sseEventSource
|
|
||||||
|
|
||||||
var sseSwapAttr = api.getAttributeValue(elt, 'sse-swap')
|
|
||||||
var sseEventNames = sseSwapAttr.split(',')
|
|
||||||
|
|
||||||
for (var i = 0; i < sseEventNames.length; i++) {
|
|
||||||
const sseEventName = sseEventNames[i].trim()
|
|
||||||
const listener = function(event) {
|
|
||||||
// If the source is missing then close SSE
|
|
||||||
if (maybeCloseSSESource(sourceElement)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the body no longer contains the element, remove the listener
|
|
||||||
if (!api.bodyContains(elt)) {
|
|
||||||
source.removeEventListener(sseEventName, listener)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// swap the response into the DOM and trigger a notification
|
|
||||||
if (!api.triggerEvent(elt, 'htmx:sseBeforeMessage', event)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
swap(elt, event.data)
|
|
||||||
api.triggerEvent(elt, 'htmx:sseMessage', event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the new listener
|
|
||||||
api.getInternalData(elt).sseEventListener = listener
|
|
||||||
source.addEventListener(sseEventName, listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add message handlers for every `hx-trigger="sse:*"` attribute
|
|
||||||
if (api.getAttributeValue(elt, 'hx-trigger')) {
|
|
||||||
// Find closest existing event source
|
|
||||||
var sourceElement = api.getClosestMatch(elt, hasEventSource)
|
|
||||||
if (sourceElement == null) {
|
|
||||||
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
|
|
||||||
return null // no eventsource in parentage, orphaned element
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set internalData and source
|
|
||||||
var internalData = api.getInternalData(sourceElement)
|
|
||||||
var source = internalData.sseEventSource
|
|
||||||
|
|
||||||
var triggerSpecs = api.getTriggerSpecs(elt)
|
|
||||||
triggerSpecs.forEach(function(ts) {
|
|
||||||
if (ts.trigger.slice(0, 4) !== 'sse:') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var listener = function (event) {
|
|
||||||
if (maybeCloseSSESource(sourceElement)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!api.bodyContains(elt)) {
|
|
||||||
source.removeEventListener(ts.trigger.slice(4), listener)
|
|
||||||
}
|
|
||||||
// Trigger events to be handled by the rest of htmx
|
|
||||||
htmx.trigger(elt, ts.trigger, event)
|
|
||||||
htmx.trigger(elt, 'htmx:sseMessage', event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the new listener
|
|
||||||
api.getInternalData(elt).sseEventListener = listener
|
|
||||||
source.addEventListener(ts.trigger.slice(4), listener)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
|
|
||||||
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
|
|
||||||
* is created and stored in the element's internalData.
|
|
||||||
* @param {HTMLElement} elt
|
|
||||||
* @param {number} retryCount
|
|
||||||
* @returns {EventSource | null}
|
|
||||||
*/
|
|
||||||
function ensureEventSourceOnElement(elt, retryCount) {
|
|
||||||
if (elt == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle extension source creation attribute
|
|
||||||
if (api.getAttributeValue(elt, 'sse-connect')) {
|
|
||||||
var sseURL = api.getAttributeValue(elt, 'sse-connect')
|
|
||||||
if (sseURL == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureEventSource(elt, sseURL, retryCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
registerSSE(elt)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureEventSource(elt, url, retryCount) {
|
|
||||||
var source = htmx.createEventSource(url)
|
|
||||||
|
|
||||||
source.onerror = function(err) {
|
|
||||||
// Log an error event
|
|
||||||
api.triggerErrorEvent(elt, 'htmx:sseError', { error: err, source })
|
|
||||||
|
|
||||||
// If parent no longer exists in the document, then clean up this EventSource
|
|
||||||
if (maybeCloseSSESource(elt)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, try to reconnect the EventSource
|
|
||||||
if (source.readyState === EventSource.CLOSED) {
|
|
||||||
retryCount = retryCount || 0
|
|
||||||
retryCount = Math.max(Math.min(retryCount * 2, 128), 1)
|
|
||||||
var timeout = retryCount * 500
|
|
||||||
window.setTimeout(function() {
|
|
||||||
ensureEventSourceOnElement(elt, retryCount)
|
|
||||||
}, timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
source.onopen = function(evt) {
|
|
||||||
api.triggerEvent(elt, 'htmx:sseOpen', { source })
|
|
||||||
|
|
||||||
if (retryCount && retryCount > 0) {
|
|
||||||
const childrenToFix = elt.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]")
|
|
||||||
for (let i = 0; i < childrenToFix.length; i++) {
|
|
||||||
registerSSE(childrenToFix[i])
|
|
||||||
}
|
|
||||||
// We want to increase the reconnection delay for consecutive failed attempts only
|
|
||||||
retryCount = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.getInternalData(elt).sseEventSource = source
|
|
||||||
|
|
||||||
|
|
||||||
var closeAttribute = api.getAttributeValue(elt, "sse-close");
|
|
||||||
if (closeAttribute) {
|
|
||||||
// close eventsource when this message is received
|
|
||||||
source.addEventListener(closeAttribute, function() {
|
|
||||||
api.triggerEvent(elt, 'htmx:sseClose', {
|
|
||||||
source,
|
|
||||||
type: 'message',
|
|
||||||
})
|
|
||||||
source.close()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* maybeCloseSSESource confirms that the parent element still exists.
|
|
||||||
* If not, then any associated SSE source is closed and the function returns true.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} elt
|
|
||||||
* @returns boolean
|
|
||||||
*/
|
|
||||||
function maybeCloseSSESource(elt) {
|
|
||||||
if (!api.bodyContains(elt)) {
|
|
||||||
var source = api.getInternalData(elt).sseEventSource
|
|
||||||
if (source != undefined) {
|
|
||||||
api.triggerEvent(elt, 'htmx:sseClose', {
|
|
||||||
source,
|
|
||||||
type: 'nodeMissing',
|
|
||||||
})
|
|
||||||
source.close()
|
|
||||||
// source = null
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} elt
|
|
||||||
* @param {string} content
|
|
||||||
*/
|
|
||||||
function swap(elt, content) {
|
|
||||||
api.withExtensions(elt, function(extension) {
|
|
||||||
content = extension.transformResponse(content, null, elt)
|
|
||||||
})
|
|
||||||
|
|
||||||
var swapSpec = api.getSwapSpecification(elt)
|
|
||||||
var target = api.getTarget(elt)
|
|
||||||
api.swap(target, content, swapSpec)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function hasEventSource(node) {
|
|
||||||
return api.getInternalData(node).sseEventSource != null
|
|
||||||
}
|
|
||||||
})()
|
|
1
public/js/htmx.min.js
vendored
1
public/js/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,18 +0,0 @@
|
|||||||
async function buildTime() {
|
|
||||||
const r = await fetch("/__livereload", { cache: "no-cache"})
|
|
||||||
return r.text()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function liveReload() {
|
|
||||||
console.log("running in development mode")
|
|
||||||
|
|
||||||
let t_old = await buildTime()
|
|
||||||
setInterval(async () => {
|
|
||||||
let t_new = await buildTime()
|
|
||||||
if (t_old !== t_new) {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
liveReload().catch(console.error)
|
|
@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/ekzyis/zaply/env"
|
"github.com/ekzyis/zaply/env"
|
||||||
"github.com/ekzyis/zaply/lightning/phoenixd"
|
"github.com/ekzyis/zaply/lightning/phoenixd"
|
||||||
"github.com/ekzyis/zaply/lnurl"
|
"github.com/ekzyis/zaply/lnurl"
|
||||||
"github.com/ekzyis/zaply/pages"
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
)
|
)
|
||||||
@ -42,11 +41,6 @@ func NewServer() *Server {
|
|||||||
|
|
||||||
lnurl.Router(s.Echo, p)
|
lnurl.Router(s.Echo, p)
|
||||||
|
|
||||||
s.Static("/", "public/")
|
|
||||||
|
|
||||||
s.GET("/overlay", pages.OverlayHandler)
|
|
||||||
s.GET("/overlay/sse", sseHandler(p.IncomingPayments()))
|
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
|
||||||
"github.com/ekzyis/zaply/components"
|
|
||||||
"github.com/ekzyis/zaply/lightning"
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Event struct {
|
|
||||||
Id []byte
|
|
||||||
Event []byte
|
|
||||||
Data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ev *Event) MarshalTo(w io.Writer) error {
|
|
||||||
if _, err := fmt.Fprintf(w, "event: %s\n", ev.Event); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, line := range bytes.Split(ev.Data, []byte("\n")) {
|
|
||||||
if _, err := fmt.Fprintf(w, "data: %s\n", line); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := fmt.Fprintf(w, "id: %s\n", ev.Id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := fmt.Fprint(w, "\n"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sseHandler(invSrc chan *lightning.Invoice) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
w := c.Response()
|
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
|
||||||
w.Header().Set("Connection", "keep-alive")
|
|
||||||
// disable nginx buffering
|
|
||||||
w.Header().Set("X-Accel-Buffering", "no")
|
|
||||||
|
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-c.Request().Context().Done():
|
|
||||||
return nil
|
|
||||||
case <-ticker.C:
|
|
||||||
event := Event{
|
|
||||||
Event: []byte("message"),
|
|
||||||
Data: []byte("keepalive"),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := event.MarshalTo(w); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case inv := <-invSrc:
|
|
||||||
buf := templ.GetBuffer()
|
|
||||||
defer templ.ReleaseBuffer(buf)
|
|
||||||
|
|
||||||
if err := components.Zap(inv).Render(c.Request().Context(), buf); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
event := Event{
|
|
||||||
Id: []byte(inv.PaymentHash),
|
|
||||||
Event: []byte("zap"),
|
|
||||||
Data: buf.Bytes(),
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("sending zap event: %s", inv.PaymentHash)
|
|
||||||
|
|
||||||
if err := event.MarshalTo(w); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.Flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: ["./components/**/*.templ", "./pages/**/*.templ"],
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user