rework data model for invitations.

This commit is contained in:
fiatjaf 2023-10-29 13:45:46 -03:00
parent f850a485cb
commit 0c94ef4e34
No known key found for this signature in database
GPG Key ID: BAD43C4BE5C1A3A1
6 changed files with 79 additions and 150 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
whitelist.json
users.json
khatru-invite
.env
khatru-badgern-db

View File

@ -25,22 +25,22 @@ func addToWhitelistHandler(w http.ResponseWriter, r *http.Request) {
pubkey = value.(string)
}
if err := addToWhitelist(r.Context(), pubkey, loggedUser); err != nil {
if err := addToWhitelist(pubkey, loggedUser); err != nil {
http.Error(w, "failed to add to whitelist: "+err.Error(), 500)
return
}
content := buildInviteTree(r.Context(), s.RelayPubkey, loggedUser)
content := buildInviteTree(r.Context(), "", loggedUser)
htmlgo.Fprint(w, content, r.Context())
}
func removeFromWhitelistHandler(w http.ResponseWriter, r *http.Request) {
loggedUser := getLoggedUser(r)
pubkey := r.PostFormValue("pubkey")
if err := removeFromWhitelist(r.Context(), pubkey, loggedUser); err != nil {
if err := removeFromWhitelist(pubkey, loggedUser); err != nil {
http.Error(w, "failed to remove from whitelist: "+err.Error(), 500)
return
}
content := buildInviteTree(r.Context(), s.RelayPubkey, loggedUser)
content := buildInviteTree(r.Context(), "", loggedUser)
htmlgo.Fprint(w, content, r.Context())
}

14
main.go
View File

@ -22,9 +22,10 @@ type Settings struct {
}
var (
db badgern.BadgerBackend
s Settings
log = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()
db badgern.BadgerBackend
s Settings
log = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()
whitelist = make(Whitelist)
)
func main() {
@ -44,20 +45,21 @@ func main() {
// load whitelist storage
if err := loadWhitelist(); err != nil {
panic(err)
log.Fatal().Err(err).Msg("failed to load whitelist")
return
}
// load db
db = badgern.BadgerBackend{Path: "./khatru-badgern-db"}
if err := db.Init(); err != nil {
panic(err)
log.Fatal().Err(err).Msg("failed to initialize database")
return
}
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
relay.RejectEvent = append(relay.RejectEvent, whitelistRejecter)
relay.Router().HandleFunc("/reports", reportsViewerHandler)
relay.Router().HandleFunc("/add-to-whitelist", addToWhitelistHandler)

View File

@ -78,31 +78,38 @@ func inviteTreePageHTML(ctx context.Context, params InviteTreePageParams) HTMLCo
Input("pubkey").Type("text").Placeholder("npub1...").Class("w-96 rounded-md border-0 p-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600"),
Button("invite").Class(buttonClass+" p-2 bg-white hover:bg-gray-50"),
Div(
buildInviteTree(ctx, s.RelayPubkey, params.LoggedUser),
buildInviteTree(ctx, "", params.LoggedUser),
).Id("tree").Class("mt-3"),
).Attr("hx-post", "/add-to-whitelist", "hx-trigger", "submit", "hx-target", "#tree")
).Attr(
"hx-post", "/add-to-whitelist",
"hx-trigger", "submit",
"hx-target", "#tree",
"_", "on htmx:afterRequest(elt, successful) if successful and elt is I call I.reset()",
)
}
func buildInviteTree(ctx context.Context, invitedBy string, loggedUser string) HTMLComponent {
func buildInviteTree(ctx context.Context, inviter string, loggedUser string) HTMLComponent {
children := make([]HTMLComponent, 0, len(whitelist))
for _, entry := range whitelist {
if entry.InvitedBy == invitedBy {
user := getUserInfo(ctx, entry.PublicKey)
for pubkey, invitedBy := range whitelist {
if invitedBy == inviter {
user := getUserInfo(ctx, pubkey)
button := Span("")
if invitedBy == loggedUser {
if isAncestorOf(loggedUser, pubkey) && loggedUser != pubkey {
button = Button("remove").
Class(buttonClass+" px-2 bg-red-100 hover:bg-red-300").
Attr("hx-post", "/remove-from-whitelist",
Attr(
"hx-post", "/remove-from-whitelist",
"hx-trigger", "click",
"hx-target", "#tree",
"hx-vals", `{"pubkey": "`+entry.PublicKey+`"}`)
"hx-vals", `{"pubkey": "`+pubkey+`"}`,
)
}
children = append(children,
Li(
A().Href("nostr:"+user.Npub).Text(user.Name).Class("font-mono py-1"),
button,
buildInviteTree(ctx, entry.PublicKey, loggedUser),
buildInviteTree(ctx, pubkey, loggedUser),
).Class("ml-4"),
)
}

View File

@ -1,58 +1,9 @@
package main
import (
"context"
"encoding/json"
"github.com/nbd-wtf/go-nostr"
)
func isPublicKeyInWhitelist(pubkey string) bool {
if pubkey == s.RelayPubkey {
return true
}
for i := 0; i < len(whitelist); i++ {
if whitelist[i].PublicKey == pubkey {
return true
}
}
return false
}
func deleteFromWhitelistRecursively(ctx context.Context, target string) {
var updatedWhitelist []WhitelistEntry
var queue []string
// Remove from whitelist
for _, user := range whitelist {
if user.PublicKey != target {
updatedWhitelist = append(updatedWhitelist, user)
}
if user.InvitedBy == target {
queue = append(queue, user.PublicKey)
}
}
whitelist = updatedWhitelist
// Remove all events
filter := nostr.Filter{
Authors: []string{target},
}
events, _ := db.QueryEvents(ctx, filter)
for ev := range events {
err := db.DeleteEvent(ctx, ev)
if err != nil {
log.Error().Err(err).Msg("failed to delete event")
}
}
// Recursive
for _, pk := range queue {
deleteFromWhitelistRecursively(ctx, pk)
}
}
func getProfileInfoFromJson(jsonStr string) (string, string) {
fieldOrder := []string{"displayName", "display_name", "username", "name"}

View File

@ -1,111 +1,79 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"github.com/nbd-wtf/go-nostr"
"golang.org/x/exp/slices"
)
type WhitelistEntry struct {
InvitedBy string `json:"invited_by"`
PublicKey string `json:"pk"`
}
const WHITELIST_FILE = "users.json"
var whitelist []WhitelistEntry
type Whitelist map[string]string // { [user_pubkey]: [invited_by] }
func whitelistRejecter(ctx context.Context, evt *nostr.Event) (reject bool, msg string) {
// check if user in whitelist
if !isPublicKeyInWhitelist(evt.PubKey) {
return true, "You are not invited to this relay"
}
/*
kind 20201
invited/whitelisted user invites new user
*/
if evt.Kind == 20201 {
}
/*
kind 20202
p tag = user removes user they invited OR admin removes user
e tag = admin removes event
*/
if evt.Kind == 20202 {
pTags := evt.Tags.GetAll([]string{"p"})
for _, tag := range pTags {
for _, user := range whitelist {
/*
1: User in whitelist
2: Cant remove self
3: User should have invited user OR be relay admin
*/
if user.PublicKey == tag.Value() && evt.PubKey != tag.Value() && (user.InvitedBy == evt.PubKey || evt.PubKey == s.RelayPubkey) {
log.Info().Str("user", tag.Value()).Msg("deleting user")
deleteFromWhitelistRecursively(ctx, tag.Value())
}
}
}
if evt.PubKey == s.RelayPubkey {
eTags := evt.Tags.GetAll([]string{"e"})
for _, tag := range eTags {
filter := nostr.Filter{
IDs: []string{tag.Value()},
}
events, _ := db.QueryEvents(ctx, filter)
for evt := range events {
log.Info().Str("event", evt.ID).Msg("deleting event")
err := db.DeleteEvent(ctx, evt)
if err != nil {
log.Warn().Err(err).Msg("failed to delete event")
}
}
}
}
}
return false, ""
}
func addToWhitelist(ctx context.Context, pubkey string, inviter string) error {
func addToWhitelist(pubkey string, inviter string) error {
if nostr.IsValidPublicKeyHex(pubkey) && isPublicKeyInWhitelist(inviter) && !isPublicKeyInWhitelist(pubkey) {
whitelist = append(whitelist, WhitelistEntry{PublicKey: pubkey, InvitedBy: inviter})
whitelist[pubkey] = inviter
}
return saveWhitelist()
}
func removeFromWhitelist(ctx context.Context, pubkey string, deleter string) error {
idx := slices.IndexFunc(whitelist, func(we WhitelistEntry) bool { return we.PublicKey == pubkey })
if idx == -1 {
return nil
func isPublicKeyInWhitelist(pubkey string) bool {
_, ok := whitelist[pubkey]
return ok
}
func isAncestorOf(pubkey string, target string) bool {
ancestor := target // we must find out if we are an ancestor of the target, but we can delete ourselves
for {
if ancestor == pubkey {
break
}
parent, ok := whitelist[ancestor]
if !ok {
// parent is not in whitelist, this means this is a top-level user and can
// only be deleted by manually editing the users.json file
return false
}
ancestor = parent
}
if whitelist[idx].InvitedBy != deleter {
return fmt.Errorf("can't remove a user you haven't invited")
return true
}
func removeFromWhitelist(target string, deleter string) error {
// check if this user is a descendant of the user who issued the delete command
if !isAncestorOf(deleter, target) {
return fmt.Errorf("insufficient permissions to delete this")
}
whitelist = append(whitelist[0:idx], whitelist[idx+1:]...)
// if we got here that means we have permission to delete the target
delete(whitelist, target)
// delete all people who were invited by the target
removeDescendantsFromWhitelist(target)
return saveWhitelist()
}
func removeDescendantsFromWhitelist(ancestor string) {
for pubkey, inviter := range whitelist {
if inviter == ancestor {
delete(whitelist, pubkey)
removeDescendantsFromWhitelist(pubkey)
}
}
}
func loadWhitelist() error {
if _, err := os.Stat("whitelist.json"); os.IsNotExist(err) {
whitelist = []WhitelistEntry{}
return nil
} else if err != nil {
return err
}
fileContent, err := os.ReadFile("whitelist.json")
b, err := os.ReadFile(WHITELIST_FILE)
if err != nil {
return err
}
if err := json.Unmarshal(fileContent, &whitelist); err != nil {
if err := json.Unmarshal(b, &whitelist); err != nil {
return err
}
@ -118,7 +86,7 @@ func saveWhitelist() error {
return err
}
if err := os.WriteFile("whitelist.json", jsonBytes, 0644); err != nil {
if err := os.WriteFile(WHITELIST_FILE, jsonBytes, 0644); err != nil {
return err
}