rewrite things to use htmlgo (wip).

This commit is contained in:
fiatjaf 2023-10-18 11:58:09 -03:00
parent 6fbde5f302
commit 32c484178f
No known key found for this signature in database
GPG Key ID: BAD43C4BE5C1A3A1
8 changed files with 219 additions and 227 deletions

3
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/kelseyhightower/envconfig v1.4.0 github.com/kelseyhightower/envconfig v1.4.0
github.com/nbd-wtf/go-nostr v0.20.0 github.com/nbd-wtf/go-nostr v0.20.0
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.31.0
github.com/theplant/htmlgo v1.0.3
) )
require ( require (
@ -52,3 +53,5 @@ require (
golang.org/x/sys v0.12.0 // indirect golang.org/x/sys v0.12.0 // indirect
google.golang.org/protobuf v1.23.0 // indirect google.golang.org/protobuf v1.23.0 // indirect
) )
replace github.com/fiatjaf/khatru => /home/fiatjaf/comp/khatru

5
go.sum
View File

@ -53,8 +53,6 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek= github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs= github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
github.com/fiatjaf/khatru v0.0.0-20231003113207-bbe186494e68 h1:oBx0uzT+KILepoPAPsjAghqAPSo9uV4pR1myr1rQQGA=
github.com/fiatjaf/khatru v0.0.0-20231003113207-bbe186494e68/go.mod h1:8shKDuVtrdLfsuHV4FBC3qYTTXnyfOLAgKqUt4u+Okk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@ -141,6 +139,9 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/theplant/htmlgo v1.0.3 h1:G7/YSf8OrOIRHVQ13avd78T/GV1kDl/jMwpQURrXB0o=
github.com/theplant/htmlgo v1.0.3/go.mod h1:pCKSFJsoVNkyW+yN2i1Mst+8130NSQzIU7L2IbnuyKg=
github.com/theplant/testingutils v0.0.0-20190603093022-26d8b4d95c61 h1:757/ruZNgTsOf5EkQBo0i3Bx/P2wgF5ljVkODeUX/uA=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=

View File

@ -3,12 +3,10 @@ package main
import ( import (
"context" "context"
"embed" "embed"
"fmt"
"html/template"
"net/http" "net/http"
"strings" "strings"
"github.com/nbd-wtf/go-nostr" "github.com/theplant/htmlgo"
) )
// embed ui files // embed ui files
@ -17,177 +15,126 @@ import (
var dist embed.FS var dist embed.FS
func inviteTreeHandler(w http.ResponseWriter, r *http.Request) { func inviteTreeHandler(w http.ResponseWriter, r *http.Request) {
formattedInviteData := buildHTMLTree(whitelist, "") content := inviteTreePageHTML(r.Context(), InviteTreePageParams{})
htmlgo.Fprint(w, baseHTML(content), r.Context())
data := map[string]interface{}{
"Relayname": s.RelayName,
"Relaydescription": s.RelayDescription,
"Pagetitle": "Invite Hierarchy",
"Pagecontent": `
<input type="text" id="inviteuser-input" placeholder="npub1..." /><button class="inviteuser">Invite!</button>
` + formattedInviteData,
}
tmpl, err := template.ParseFS(dist, "ui/dist/index.html")
if err != nil {
http.Error(w, "Error parsing template: "+err.Error(), http.StatusInternalServerError)
return
}
err = tmpl.Execute(w, data)
if err != nil {
http.Error(w, "Error executing template: "+err.Error(), http.StatusInternalServerError)
return
}
} }
func reportsViewerHandler(w http.ResponseWriter, r *http.Request) { func reportsViewerHandler(w http.ResponseWriter, r *http.Request) {
var formattedReportsData template.HTML = "" // var formattedReportsData template.HTML = ""
events, _ := db.QueryEvents(context.Background(), nostr.Filter{ // events, _ := db.QueryEvents(context.Background(), nostr.Filter{
Kinds: []int{1984}, // Kinds: []int{1984},
Limit: 52, // Limit: 52,
}) // })
type Report struct { // type Report struct {
ID string // ID string
ByUser string // ByUser string
AboutUser string // AboutUser string
AboutEvent string // AboutEvent string
Type string // Type string
Content string // Content string
} // }
for ev := range events { // for ev := range events {
pTag := ev.Tags.GetFirst([]string{"p"}) // pTag := ev.Tags.GetFirst([]string{"p"})
eTag := ev.Tags.GetFirst([]string{"e"}) // eTag := ev.Tags.GetFirst([]string{"e"})
if pTag != nil { // if pTag != nil {
typeReport := eTag.Relay()[6:] // typeReport := eTag.Relay()[6:]
if typeReport == "" { // if typeReport == "" {
typeReport = pTag.Relay()[6:] // typeReport = pTag.Relay()[6:]
} // }
report := Report{ // report := Report{
ID: ev.ID, // ID: ev.ID,
ByUser: ev.PubKey, // ByUser: ev.PubKey,
AboutUser: pTag.Value(), // AboutUser: pTag.Value(),
AboutEvent: eTag.Value(), // AboutEvent: eTag.Value(),
Type: typeReport, // Type: typeReport,
Content: ev.Content, // Content: ev.Content,
} // }
// get AboutEvent content, note1 ect // // get AboutEvent content, note1 ect
formattedReportsData += template.HTML(fmt.Sprintf(` // formattedReportsData += template.HTML(fmt.Sprintf(`
<div> // <div>
<p><b>Report %v</b></p> // <p><b>Report %v</b></p>
<p>By User: <a class="user" href="nostr:%v">%v</a></p> // <p>By User: <a class="user" href="nostr:%v">%v</a></p>
<p>About User: <a class="user" href="nostr:%v">%v</a></p>`, // <p>About User: <a class="user" href="nostr:%v">%v</a></p>`,
report.ID, // report.ID,
getUserInfo(context.Background(), report.ByUser).Npub, // getUserInfo(context.Background(), report.ByUser).Npub,
getUserInfo(context.Background(), report.ByUser).Name, // getUserInfo(context.Background(), report.ByUser).Name,
getUserInfo(context.Background(), report.AboutUser).Npub, // getUserInfo(context.Background(), report.AboutUser).Npub,
getUserInfo(context.Background(), report.AboutUser).Name, // getUserInfo(context.Background(), report.AboutUser).Name,
)) // ))
if report.AboutEvent != "" { // if report.AboutEvent != "" {
// fetch event data // // fetch event data
aboutEvents, _ := db.QueryEvents(context.TODO(), nostr.Filter{ // aboutEvents, _ := db.QueryEvents(context.TODO(), nostr.Filter{
IDs: []string{report.AboutEvent}, // IDs: []string{report.AboutEvent},
}) // })
for aboutEvent := range aboutEvents { // for aboutEvent := range aboutEvents {
formattedReportsData += template.HTML(fmt.Sprintf(` // formattedReportsData += template.HTML(fmt.Sprintf(`
<p> // <p>
About Event: <ul> // About Event: <ul>
<p>Kind: %v</p> // <p>Kind: %v</p>
<p>Tags: %v</p> // <p>Tags: %v</p>
<p>Content: %v</p> // <p>Content: %v</p>
</ul> // </ul>
</p>`, // </p>`,
template.HTMLEscaper(aboutEvent.Kind), // template.HTMLEscaper(aboutEvent.Kind),
template.HTMLEscaper(aboutEvent.Tags), // template.HTMLEscaper(aboutEvent.Tags),
template.HTMLEscaper(aboutEvent.Content), // template.HTMLEscaper(aboutEvent.Content),
)) // ))
} // }
} // }
formattedReportsData += template.HTML(fmt.Sprintf(` // formattedReportsData += template.HTML(fmt.Sprintf(`
<p>Type: %v</p>`, // <p>Type: %v</p>`,
report.Type, // report.Type,
)) // ))
if report.Content != "" { // if report.Content != "" {
formattedReportsData += template.HTML(fmt.Sprintf(` // formattedReportsData += template.HTML(fmt.Sprintf(`
<p>Content: %v</p> // <p>Content: %v</p>
<div> // <div>
<button data-actionarg='[["e", "%v"],["p", "%v"]]' class="removefromrelay">Ban Reported User and Remove Report</button> // <button data-actionarg='[["e", "%v"],["p", "%v"]]' class="removefromrelay">Ban Reported User and Remove Report</button>
<button data-actionarg='[["e", "%v"]]' class="removefromrelay">Remove This Report</button> // <button data-actionarg='[["e", "%v"]]' class="removefromrelay">Remove This Report</button>
<button data-actionarg='[["p", "%v"]]' class="removefromrelay">Ban User who wrote report</button> // <button data-actionarg='[["p", "%v"]]' class="removefromrelay">Ban User who wrote report</button>
</div> // </div>
</div> // </div>
<hr />`, // <hr />`,
template.HTMLEscaper(report.Content), // template.HTMLEscaper(report.Content),
template.HTMLEscaper(report.ID), // template.HTMLEscaper(report.ID),
template.HTMLEscaper(report.AboutUser), // template.HTMLEscaper(report.AboutUser),
template.HTMLEscaper(report.ID), // template.HTMLEscaper(report.ID),
template.HTMLEscaper(report.ByUser), // template.HTMLEscaper(report.ByUser),
)) // ))
} // }
} // }
} // }
data := map[string]interface{}{ // data := map[string]interface{}{
"Relayname": s.RelayName, // "Relayname": s.RelayName,
"Relaydescription": s.RelayDescription, // "Relaydescription": s.RelayDescription,
"Pagetitle": "Reports Viewer", // "Pagetitle": "Reports Viewer",
"Pagecontent": formattedReportsData, // "Pagecontent": formattedReportsData,
} // }
tmpl, err := template.ParseFS(dist, "ui/dist/index.html") // tmpl, err := template.ParseFS(dist, "ui/dist/index.html")
if err != nil { // if err != nil {
http.Error(w, "Error parsing template: "+err.Error(), http.StatusInternalServerError) // http.Error(w, "Error parsing template: "+err.Error(), http.StatusInternalServerError)
return // return
} // }
// Execute the template with the provided data and write it to the response // // Execute the template with the provided data and write it to the response
err = tmpl.Execute(w, data) // err = tmpl.Execute(w, data)
if err != nil { // if err != nil {
http.Error(w, "Error executing template: "+err.Error(), http.StatusInternalServerError) // http.Error(w, "Error executing template: "+err.Error(), http.StatusInternalServerError)
return // return
} // }
} }
func homePageHandler(w http.ResponseWriter, r *http.Request) { func homePageHandler(w http.ResponseWriter, r *http.Request) {
relayOwnerInfo := getUserInfo(context.Background(), s.RelayPubkey) content := homePageHTML(r.Context(), HomePageParams{
RelayOwnerInfo: getUserInfo(context.Background(), s.RelayPubkey),
data := map[string]interface{}{ })
"Relayname": s.RelayName, htmlgo.Fprint(w, baseHTML(content), r.Context())
"Relaydescription": s.RelayDescription,
"Pagetitle": "Info",
"Pagecontent": template.HTML(fmt.Sprintf(`
<div>Relay Name: %v</div>
<div>Relay Description: %v</div>
<div>Relay Owner: <a class="user" href="nostr:%v">%v</a></div>
<div>Relay Alternative Contact: %v</div>
<br />
<div><sub>This relay uses <a target="_blank" rel="noopener noreferrer" href="https://github.com/github-tijlxyz/khatru-invite">Khatru Invite</a>, which is build with <a target="_blank" rel="noopener noreferrer" href="https://github.com/fiatjaf/khatru">Khatru</a></sub></div>
`, s.RelayName, s.RelayDescription, relayOwnerInfo.Npub, relayOwnerInfo.Name, s.RelayContact)),
}
tmpl, err := template.ParseFS(dist, "ui/dist/index.html")
if err != nil {
http.Error(w, "Error parsing template: "+err.Error(), http.StatusInternalServerError)
return
}
err = tmpl.Execute(w, data)
if err != nil {
http.Error(w, "Error executing template: "+err.Error(), http.StatusInternalServerError)
return
}
}
func redirectHandler(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/assets") {
staticHandler("ui/dist", w, r)
} else if r.URL.Path == "/" {
homePageHandler(w, r)
}
} }
func staticHandler(prefix string, w http.ResponseWriter, r *http.Request) { func staticHandler(prefix string, w http.ResponseWriter, r *http.Request) {

View File

@ -16,6 +16,7 @@ import (
) )
type Settings struct { type Settings struct {
Port string `envconfig:"PORT" default:"3334"`
RelayName string `envconfig:"RELAY_NAME" required:"true"` RelayName string `envconfig:"RELAY_NAME" required:"true"`
RelayPubkey string `envconfig:"RELAY_PUBKEY" required:"true"` RelayPubkey string `envconfig:"RELAY_PUBKEY" required:"true"`
RelayDescription string `envconfig:"RELAY_DESCRIPTION"` RelayDescription string `envconfig:"RELAY_DESCRIPTION"`
@ -83,10 +84,12 @@ func main() {
// ui // ui
relay.Router().HandleFunc("/reports", reportsViewerHandler) relay.Router().HandleFunc("/reports", reportsViewerHandler)
relay.Router().HandleFunc("/users", inviteTreeHandler) relay.Router().HandleFunc("/users", inviteTreeHandler)
relay.Router().HandleFunc("/", redirectHandler) relay.Router().HandleFunc("/", homePageHandler)
log.Info().Msg("running on http://127.0.0.1:3334") log.Info().Msg("running on http://0.0.0.0:" + s.Port)
http.ListenAndServe(":3334", relay) if err := http.ListenAndServe(":"+s.Port, relay); err != nil {
log.Fatal().Err(err).Msg("failed to serve")
}
} }
// save whitelist on shutdown // save whitelist on shutdown

92
pages.go Normal file
View File

@ -0,0 +1,92 @@
package main
import (
"context"
. "github.com/theplant/htmlgo"
)
func baseHTML(inside HTMLComponent) HTMLComponent {
navItemClass := "text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 font-medium"
return HTML(
Head(
Meta().Charset("utf-8"),
Meta().Name("viewport").Content("width=device-width, initial-scale=1"),
Title(s.RelayName),
Script("").Src("https://cdn.tailwindcss.com"),
),
Body(
Div(
H1(s.RelayName).Class("font-bold text-2xl"),
P().Text(s.RelayDescription).Class("text-lg"),
).Class("mx-auto my-6 text-center"),
Nav(
A().Text("information").Href("/").Class(navItemClass),
A().Text("invite tree").Href("/users").Class(navItemClass),
A().Text("reports").Href("/reports").Class(navItemClass),
).Class("flex flex-1 items-center justify-center"),
Div(inside).Class("m-4"),
).Class("bg-gray-800 mx-4 my-6 text-white"),
)
}
type HomePageParams struct {
RelayOwnerInfo SimpleUserInfo
}
func homePageHTML(ctx context.Context, params HomePageParams) HTMLComponent {
contact := Div()
if s.RelayContact != "" {
contact = Div().Text("alternative contact: " + s.RelayContact)
}
description := Div()
if s.RelayDescription != "" {
description = Div().Text("description: " + s.RelayDescription)
}
return Div(
Div().Text("name: "+s.RelayName),
description,
contact,
Div(
Text("relay master: "),
A().Text(params.RelayOwnerInfo.Name).Href("nostr:"+params.RelayOwnerInfo.Npub),
),
Br(),
Div(
Text("this relay uses"),
A().Target("_blank").Href("https://github.com/github-tijlxyz/khatru-invite").Text("Khatru Invite"),
Text(" which is built with "),
A().Target("_blank").Href("https://github.com/fiatjaf/khatru").Text("Khatru"),
),
)
}
type InviteTreePageParams struct{}
func inviteTreePageHTML(ctx context.Context, params InviteTreePageParams) HTMLComponent {
return Div(
Input("").Type("text").Placeholder("npub1..."),
Button("invite"),
buildInviteTree(ctx, ""),
)
}
func buildInviteTree(ctx context.Context, invitedBy string) HTMLComponent {
tree := Ul()
for _, entry := range whitelist {
if entry.InvitedBy == invitedBy {
user := getUserInfo(ctx, entry.PublicKey)
tree = tree.Children(
Li(
A().Href("nostr:"+user.Npub).Text(user.Name),
A().Text("remove"),
buildInviteTree(ctx, entry.PublicKey),
),
)
}
}
return tree
}

View File

@ -1,32 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{.Relaydescription}}">
<script src="./src/main.ts" type="module"></script>
<title>{{.Relayname}} | {{.Pagetitle}}</title>
</head>
<body>
<div>
<h1>{{.Relayname}}</h1>
<p>{{.Relaydescription}}</p>
</div>
<hr />
<div>
<span>
<a href="/">information</a> -
<a href="/users">invite tree</a> -
<a href="/reports">admin reports viewer</a>
</span>
</div>
<hr />
<br />
<div>
{{.Pagecontent}}
</div>
</body>
</html>

View File

@ -3,37 +3,17 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"html/template"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
) )
func buildHTMLTree(entries []WhitelistEntry, invitedBy string) template.HTML { func isPublicKeyInWhitelist(pubkey string) bool {
html := "<ul>" if pubkey == s.RelayPubkey {
return true
for _, entry := range entries {
if entry.InvitedBy == invitedBy {
user := getUserInfo(context.TODO(), entry.PublicKey)
html += fmt.Sprintf(`
<li>
<a class="user" href="nostr:%s">%s</a>
<a data-actionarg='[["p", "%v"]]' class="rembtn removefromrelay">x</a>
%s
</li>`, template.HTMLEscapeString(user.Npub),
template.HTMLEscapeString(user.Name),
entry.PublicKey,
buildHTMLTree(entries, entry.PublicKey))
}
} }
html += "</ul>"
return template.HTML(html)
}
func isPkInWhitelist(targetPk string) bool {
for i := 0; i < len(whitelist); i++ { for i := 0; i < len(whitelist); i++ {
if whitelist[i].PublicKey == targetPk { if whitelist[i].PublicKey == pubkey {
return true return true
} }
} }

View File

@ -17,7 +17,7 @@ var whitelist []WhitelistEntry
func whitelistRejecter(ctx context.Context, evt *nostr.Event) (reject bool, msg string) { func whitelistRejecter(ctx context.Context, evt *nostr.Event) (reject bool, msg string) {
// check if user in whitelist // check if user in whitelist
if !isPkInWhitelist(evt.PubKey) { if !isPublicKeyInWhitelist(evt.PubKey) {
return true, "You are not invited to this relay" return true, "You are not invited to this relay"
} }
@ -28,10 +28,8 @@ func whitelistRejecter(ctx context.Context, evt *nostr.Event) (reject bool, msg
if evt.Kind == 20201 { if evt.Kind == 20201 {
pTags := evt.Tags.GetAll([]string{"p"}) pTags := evt.Tags.GetAll([]string{"p"})
for _, tag := range pTags { for _, tag := range pTags {
if !isPkInWhitelist(tag.Value()) { if nostr.IsValidPublicKeyHex(tag.Value()) && !isPublicKeyInWhitelist(tag.Value()) {
if nostr.IsValidPublicKeyHex(tag.Value()) { whitelist = append(whitelist, WhitelistEntry{PublicKey: tag.Value(), InvitedBy: evt.PubKey})
whitelist = append(whitelist, WhitelistEntry{PublicKey: tag.Value(), InvitedBy: evt.PubKey})
}
} }
} }
} }