From 123d51e011b68b13a975dc4dcadb1938ea57133d Mon Sep 17 00:00:00 2001 From: Juliusz Chroboczek Date: Sun, 11 Aug 2024 23:57:11 +0200 Subject: [PATCH] Implement selective clearchat. --- group/group.go | 36 ++++++++++++++++++++++++++++-- group/group_test.go | 36 +++++++++++++++++++++++++----- rtpconn/webclient.go | 19 +++++++++++++++- static/galene.js | 53 ++++++++++++++++++++++++++++++++++++++------ 4 files changed, 129 insertions(+), 15 deletions(-) diff --git a/group/group.go b/group/group.go index c17e893..b651b68 100644 --- a/group/group.go +++ b/group/group.go @@ -799,10 +799,42 @@ func (g *Group) WallOps(message string) { const maxChatHistory = 50 -func (g *Group) ClearChatHistory() { +// deleteFunc is just like slices.DeleteFunc. +// Remove this once we require Go 1.21. +func deleteFunc[S ~[]E, E any](s S, f func(E) bool) S { + i := 0 + for i = range s { + if f(s[i]) { + break + } + } + if i >= len(s) { + return s + } + + for j := i + 1; j < len(s); j++ { + if v := s[j]; !f(v) { + s[i] = v + i++ + } + } + var zero E + for j := i; j < len(s); j++ { + s[j] = zero + } + return s[:i] +} + +func (g *Group) ClearChatHistory(id string, userId string) { g.mu.Lock() defer g.mu.Unlock() - g.history = nil + if id == "" && userId == "" { + g.history = nil + return + } + g.history = deleteFunc(g.history, func(e ChatHistoryEntry) bool { + return e.Source == userId && (id == "" || e.Id == id) + }) } func (g *Group) AddToChatHistory(id, source string, user *string, time time.Time, kind string, value interface{}) { diff --git a/group/group_test.go b/group/group_test.go index 0368d53..3e2a5bb 100644 --- a/group/group_test.go +++ b/group/group_test.go @@ -4,9 +4,9 @@ import ( "encoding/json" "errors" "fmt" + "sort" "testing" "time" - "sort" ) func TestGroup(t *testing.T) { @@ -54,7 +54,10 @@ func TestChatHistory(t *testing.T) { } user := "user" for i := 0; i < 2*maxChatHistory; i++ { - g.AddToChatHistory("id", "source", &user, time.Now(), "", + g.AddToChatHistory( + fmt.Sprintf("id-%v", i), + fmt.Sprintf("source-%v", i%4), + &user, time.Now(), "", fmt.Sprintf("%v", i), ) } @@ -63,10 +66,33 @@ func TestChatHistory(t *testing.T) { t.Errorf("Expected %v, got %v", maxChatHistory, len(g.history)) } for i, s := range h { - e := fmt.Sprintf("%v", i+maxChatHistory) - if s.Value.(string) != e { - t.Errorf("Expected %v, got %v", e, s) + j := i + maxChatHistory + if s.Id != fmt.Sprintf("id-%v", j) { + t.Errorf("Expected %v, got %v", j, s.Id) } + if s.Source != fmt.Sprintf("source-%v", j%4) { + t.Errorf("Expected %v, got %v", j%4, s.Id) + } + if s.Value.(string) != fmt.Sprintf("%v", j) { + t.Errorf("Expected %v, got %v", j, s.Value) + } + } + + l := len(h) + j := maxChatHistory + 4 + g.ClearChatHistory( + fmt.Sprintf("id-%v", j), fmt.Sprintf("source-%v", j%4), + ) + if lh := len(g.GetChatHistory()); lh != l-1 { + t.Errorf("Expected %v, got %v", l-1, lh) + } + g.ClearChatHistory("", fmt.Sprintf("source-%v", j%4)) + if lh := len(g.GetChatHistory()); lh != l*3/4 { + t.Errorf("Expected %v, got %v", l*3/4, lh) + } + g.ClearChatHistory("", ""); + if lh := len(g.GetChatHistory()); lh != 0 { + t.Errorf("Expected 0, got %v", lh) } } diff --git a/rtpconn/webclient.go b/rtpconn/webclient.go index a606418..2fe9842 100644 --- a/rtpconn/webclient.go +++ b/rtpconn/webclient.go @@ -1636,10 +1636,27 @@ func handleClientMessage(c *webClient, m clientMessage) error { if !member("op", c.permissions) { return c.error(group.UserError("not authorised")) } - g.ClearChatHistory() + var id, userId string + if m.Value != nil { + value, ok := m.Value.(map[string]any) + if !ok { + return c.error(group.UserError( + "bad value in clearchat", + )) + } + id, _ = value["id"].(string) + userId, _ = value["userId"].(string) + if userId == "" && id != "" { + return c.error(group.UserError( + "bad value in clearchat", + )) + } + } + g.ClearChatHistory(id, userId) m := clientMessage{ Type: "usermessage", Kind: "clearchat", + Value: m.Value, Privileged: true, } err := broadcast(g.GetClients(nil), m) diff --git a/static/galene.js b/static/galene.js index 12d1d46..19d4945 100644 --- a/static/galene.js +++ b/static/galene.js @@ -2781,13 +2781,16 @@ function gotUserMessage(id, dest, username, time, privileged, kind, error, messa let by = username ? ' by ' + username : ''; displayWarning(`You have been muted${by}`); break; - case 'clearchat': + case 'clearchat': { if(!privileged) { console.error(`Got unprivileged message of kind ${kind}`); return; } - clearChat(); + let id = message && message.id; + let userId = message && message.userId; + clearChat(id, userId); break; + } case 'token': if(!privileged) { console.error(`Got unprivileged message of kind ${kind}`); @@ -3071,17 +3074,31 @@ function chatMessageMenu(elt) { serverConnection.permissions.indexOf('op') >= 0)) return; + let messageId = elt.dataset.id; let peerId = elt.dataset.peerId; if(!peerId) return; let username = elt.dataset.username; - let u = username ? ' ' + username : ''; + let u = username || 'user'; let items = []; - items.push({label: 'Identify user' + u, onClick: () => { + if(messageId) + items.push({label: 'Delete message', onClick: () => { + serverConnection.groupAction('clearchat', { + id: messageId, + userId: peerId, + }); + }}); + items.push({label: `Delete all from ${u}`, + onClick: () => { + serverConnection.groupAction('clearchat', { + userId: peerId, + }); + }}); + items.push({label: `Identify ${u}`, onClick: () => { serverConnection.userAction('identify', peerId); }}); - items.push({label: 'Kick out user' + u, onClick: () => { + items.push({label: `Kick out ${u}`, onClick: () => { serverConnection.userAction('kick', peerId); }}); @@ -3098,9 +3115,31 @@ function localMessage(message) { return addToChatbox(null, null, null, null, new Date(), false, false, '', message); } -function clearChat() { +/** + * @param {string} [id] + * @param {string} [userId] + */ +function clearChat(id, userId) { lastMessage = {}; - document.getElementById('box').textContent = ''; + + let box = document.getElementById('box'); + if(!id && !userId) { + box.textContent = ''; + return; + } + + let elts = box.children; + for(let i = 0; i < elts.length; i++) { + let row = elts.item(i); + if(!(row instanceof HTMLDivElement)) + continue; + let div = row.firstChild; + console.log(div); + if(!(div instanceof HTMLDivElement)) + continue; + if((!id || div.dataset.id === id) && div.dataset.peerId === userId) + box.removeChild(row); + } } /**