From 5f6845c3997bd5478a1f51a8f108b71afa427a72 Mon Sep 17 00:00:00 2001 From: Kyren223 Date: Mon, 21 Jul 2025 13:46:40 +0300 Subject: [PATCH] Added the ability to delete users --- internal/client/gateway/gateway.go | 4 +- internal/client/ui/core/core.go | 2 + internal/client/ui/core/state/state.go | 25 ++++++++++- .../ui/core/usersettings/usersettings.go | 42 ++++++++++++++++++- internal/data/users.sql.go | 11 +++-- internal/server/api/api.go | 18 ++++++++ internal/server/server.go | 10 ++--- query/users.sql | 2 +- 8 files changed, 101 insertions(+), 13 deletions(-) diff --git a/internal/client/gateway/gateway.go b/internal/client/gateway/gateway.go index f543d82..68c26b2 100644 --- a/internal/client/gateway/gateway.go +++ b/internal/client/gateway/gateway.go @@ -148,7 +148,7 @@ func handlePacketStream() { payload, err := pkt.DecodedPayload() assert.NoError(err, "server should always provide a decodeable packet") - log.Printf("received streamed packet %v: %v\n", payload.Type(), payload) + // log.Printf("received streamed packet %v: %v\n", payload.Type(), payload) ui.Program.Send(payload) } } @@ -189,7 +189,7 @@ func Send(request packet.Payload) tea.Cmd { if err != nil { log.Println("request send error:", err) } else { - log.Println("request sent successfully:", request) + // log.Println("request sent successfully:", request) } return RequestSentMsg{ request: request, diff --git a/internal/client/ui/core/core.go b/internal/client/ui/core/core.go index 8c0a336..7e42619 100644 --- a/internal/client/ui/core/core.go +++ b/internal/client/ui/core/core.go @@ -388,6 +388,7 @@ func (m *Model) updateConnected(message tea.Msg) tea.Cmd { } else { log.Println("received error:", msg.Error) gateway.Disconnect() + state.Reset() return ui.Transition(ui.NewAuth()) } } @@ -490,6 +491,7 @@ func (m *Model) updateAuthenticated(message tea.Msg) tea.Cmd { err := "new connection from another location, closing this one" if err == msg.Error { gateway.Disconnect() + state.Reset() return ui.Transition(ui.NewAuth()) } diff --git a/internal/client/ui/core/state/state.go b/internal/client/ui/core/state/state.go index 16b92a9..97a6de8 100644 --- a/internal/client/ui/core/state/state.go +++ b/internal/client/ui/core/state/state.go @@ -84,6 +84,29 @@ var Data UserData = UserData{ var UserID *snowflake.ID = nil +func Reset() { + UserID = nil + Data = UserData{ + Networks: []snowflake.ID{}, + Signals: []snowflake.ID{}, + } + State = state{ + ChatState: map[snowflake.ID]ChatState{}, + LastFrequency: map[snowflake.ID]snowflake.ID{}, + Messages: map[snowflake.ID]*btree.BTreeG[data.Message]{}, + Networks: map[snowflake.ID]data.Network{}, + Frequencies: map[snowflake.ID][]data.Frequency{}, + Members: map[snowflake.ID]map[snowflake.ID]data.Member{}, + Users: map[snowflake.ID]data.User{}, + TrustedUsers: map[snowflake.ID]ed25519.PublicKey{}, + BlockedUsers: map[snowflake.ID]struct{}{}, + BlockingUsers: map[snowflake.ID]struct{}{}, + LastReadMessages: map[snowflake.ID]*snowflake.ID{}, + RemoteNotifications: map[snowflake.ID]int{}, + LocalNotifications: map[snowflake.ID]int{}, + } +} + func UpdateNetworks(info *packet.NetworksInfo) { networks := State.Networks @@ -382,7 +405,7 @@ func SendFinalData() { // HACK: Give a small grace period for the writes to be processed // Tweak this value as needed - time.Sleep(20 * time.Millisecond) + // time.Sleep(20 * time.Millisecond) // TODO: // I think the issue is that it's random which of these 2 requests goes diff --git a/internal/client/ui/core/usersettings/usersettings.go b/internal/client/ui/core/usersettings/usersettings.go index c833b5c..f7664fc 100644 --- a/internal/client/ui/core/usersettings/usersettings.go +++ b/internal/client/ui/core/usersettings/usersettings.go @@ -18,12 +18,14 @@ package usersettings import ( "errors" + "log" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/kyren223/eko/internal/client/config" "github.com/kyren223/eko/internal/client/gateway" + "github.com/kyren223/eko/internal/client/ui" "github.com/kyren223/eko/internal/client/ui/colors" "github.com/kyren223/eko/internal/client/ui/core/state" "github.com/kyren223/eko/internal/client/ui/field" @@ -46,6 +48,17 @@ var ( Render("Update User Settings") } + blurredDelete = func() string { + return lipgloss.NewStyle().Padding(0, 1). + Background(colors.DarkGray).Foreground(colors.Red). + Render("Delete User Permanently") + } + focusedDelete = func() string { + return lipgloss.NewStyle().Padding(0, 1). + Background(colors.Red).Foreground(colors.Black). + Render("Delete User Permanently") + } + highlightedStyle = func() lipgloss.Style { return lipgloss.NewStyle().Padding(0, 0). Background(colors.BackgroundHighlight).Foreground(colors.White) @@ -57,6 +70,7 @@ const ( Description PrivateField UpdateField + DeleteField FieldCount ) @@ -65,6 +79,7 @@ type Model struct { description field.Model privateDM bool update string + delete string selected int nameWidth int @@ -126,6 +141,7 @@ func New() Model { description: description, privateDM: !user.IsPublicDM, update: blurredUpdate(), + delete: blurredDelete(), selected: 0, nameWidth: nameWidth, } @@ -158,6 +174,12 @@ func (m Model) View() string { Align(lipgloss.Center). Render(m.update) + del := lipgloss.NewStyle(). + Width(m.nameWidth). + Background(colors.Background). + Align(lipgloss.Center). + Render(m.delete) + configFile := "Config File: " + highlightedStyle().Render(config.ConfigFile) configFile = lipgloss.NewStyle(). Background(colors.Background).Foreground(colors.White). @@ -172,7 +194,7 @@ func (m Model) View() string { Render(analyticsOptOut) content := flex.NewVertical( - analyticsOptOut, configFile, name, description, private, update, + analyticsOptOut, configFile, name, description, private, update, del, ).WithGap(1).View() return lipgloss.NewStyle(). @@ -225,6 +247,7 @@ func (m *Model) updateFocus() tea.Cmd { m.name.Blur() m.description.Blur() m.update = blurredUpdate() + m.delete = blurredDelete() switch m.selected { case NameField: return m.name.Focus() @@ -235,6 +258,9 @@ func (m *Model) updateFocus() tea.Cmd { case UpdateField: m.update = focusedUpdate() return nil + case DeleteField: + m.delete = focusedDelete() + return nil default: assert.Never("missing switch statement field in update focus", "selected", m.selected) return nil @@ -247,6 +273,20 @@ func (m *Model) Select() tea.Cmd { return nil } + if m.selected == DeleteField { + log.Println("DELETING CLIENT") + // PERMA DELETE USER and return to login screen + state.Reset() + return tea.Batch(func() tea.Msg { + msg := gateway.Send(&packet.SetUserData{ + Data: nil, + User: &data.User{IsDeleted: true}, + })() // Important, call this function to block + gateway.Disconnect() + return msg + }, ui.Transition(ui.NewAuth())) + } + if m.selected != UpdateField { return nil } diff --git a/internal/data/users.sql.go b/internal/data/users.sql.go index 97d622e..078af15 100644 --- a/internal/data/users.sql.go +++ b/internal/data/users.sql.go @@ -45,12 +45,17 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e const deleteUser = `-- name: DeleteUser :exec UPDATE users SET - is_deleted = true + is_deleted = true, public_key = ?, name = "Deleted User" WHERE id = ? AND is_deleted = false ` -func (q *Queries) DeleteUser(ctx context.Context, id snowflake.ID) error { - _, err := q.db.ExecContext(ctx, deleteUser, id) +type DeleteUserParams struct { + PublicKey ed25519.PublicKey + ID snowflake.ID +} + +func (q *Queries) DeleteUser(ctx context.Context, arg DeleteUserParams) error { + _, err := q.db.ExecContext(ctx, deleteUser, arg.PublicKey, arg.ID) return err } diff --git a/internal/server/api/api.go b/internal/server/api/api.go index 35ca44c..5f11070 100644 --- a/internal/server/api/api.go +++ b/internal/server/api/api.go @@ -842,6 +842,24 @@ func SetUserData(ctx context.Context, sess *session.Session, request *packet.Set var userPtr *data.User = nil if request.User != nil { + if request.User.IsDeleted { + // Delete user + + pubKey, _, err := ed25519.GenerateKey(nil) + assert.NoError(err, "random should never fail") + err = queries.DeleteUser(ctx, data.DeleteUserParams{ + PublicKey: pubKey, + ID: sess.ID(), + }) + if err != nil { + slog.ErrorContext(ctx, "database error", "error", err) + return &ErrInternalError + } + + sess.Close() // Close connection and respond with nothing + return nil + } + name := request.User.Name if len(name) > packet.MaxUsernameBytes { return &packet.Error{Error: fmt.Sprintf( diff --git a/internal/server/server.go b/internal/server/server.go index 48d7d38..15c3a7a 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -60,7 +60,7 @@ const ( RateLimitCountThresholdMalicious = 10 ) -func getTlsConfig() *tls.Config { +func getTLSConfig() *tls.Config { path, ok := os.LookupEnv(CertFile) if !ok { // DEV MODE ONLY, DUMMY CERT @@ -219,7 +219,7 @@ func (s *server) Node() *snowflake.Node { func (s *server) Run() { slog.Info("starting eko-server...") - listener, err := tls.Listen("tcp4", ":"+strconv.Itoa(int(s.Port)), getTlsConfig()) + listener, err := tls.Listen("tcp4", ":"+strconv.Itoa(int(s.Port)), getTLSConfig()) if err != nil { slog.Error("error starting server", "error", err) assert.Abort("see logs") @@ -286,7 +286,7 @@ func (server *server) handleConnection(conn net.Conn) { defer conn.Close() var writerWg sync.WaitGroup - done := make(chan struct{}) + writeDone := make(chan struct{}) framer := packet.NewFramer() sess := session.NewSession(server, addr, cancel, &writerWg) @@ -306,7 +306,7 @@ func (server *server) handleConnection(conn net.Conn) { // Writer go func() { - defer close(done) + defer close(writeDone) defer conn.Close() // To unblock reader writeQueue := sess.Read() @@ -406,7 +406,7 @@ func (server *server) handleConnection(conn net.Conn) { close(framer.Out) // stop processing slog.InfoContext(ctx, "reader done, closed framer") - <-done + <-writeDone } func processPacket(ctx context.Context, sess *session.Session, pkt packet.Packet) bool { diff --git a/query/users.sql b/query/users.sql index 50ec9af..ce075cd 100644 --- a/query/users.sql +++ b/query/users.sql @@ -16,7 +16,7 @@ RETURNING *; -- name: DeleteUser :exec UPDATE users SET - is_deleted = true + is_deleted = true, public_key = ?, name = "Deleted User", description = "" WHERE id = ? AND is_deleted = false; -- name: SetUserData :one