From 48f8c341d76d601f3fe2d97406f327d302a730de Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 31 May 2023 07:16:14 +0200 Subject: [PATCH] Implement draft/message-redaction (#2065) * Makefile: Add dependencies between targets * Implement draft/message-redaction for channels Permission to use REDACT mirrors permission for 'HistServ DELETE' * Error when the given targetmsg does not exist * gofmt * Add CanDelete enum type * gofmt * Add support for PMs * Fix documentation of allow-individual-delete. * Remove 'TODO: add configurable fallback' slingamn says it's probably not desirable, and I'm on the fence. Out of laziness, let's omit it for now, as it's not a regression compared to '/msg HistServ DELETE'. * Revert "Makefile: Add dependencies between targets" This reverts commit 2182b1da69ceaafad30859e45be0645d6c915b2c. --------- Co-authored-by: Val Lorentz --- default.yaml | 3 +- gencapdefs.py | 6 ++++ irc/caps/defs.go | 9 +++-- irc/commands.go | 4 +++ irc/handlers.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++++ irc/help.go | 6 ++++ irc/histserv.go | 50 ++++++++++++++++++++------ traditional.yaml | 3 +- 8 files changed, 157 insertions(+), 15 deletions(-) diff --git a/default.yaml b/default.yaml index 23573f6c..93cef8df 100644 --- a/default.yaml +++ b/default.yaml @@ -982,7 +982,8 @@ history: # options to control how messages are stored and deleted: retention: - # allow users to delete their own messages from history? + # allow users to delete their own messages from history, + # and channel operators to delete messages in their channel? allow-individual-delete: false # if persistent history is enabled, create additional index tables, diff --git a/gencapdefs.py b/gencapdefs.py index 19ca4baa..00ba435e 100644 --- a/gencapdefs.py +++ b/gencapdefs.py @@ -87,6 +87,12 @@ CAPDEFS = [ url="https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6", standard="proposed IRCv3", ), + CapDef( + identifier="MessageRedaction", + name="draft/message-redaction", + url="https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md", + standard="proposed IRCv3", + ), CapDef( identifier="MessageTags", name="message-tags", diff --git a/irc/caps/defs.go b/irc/caps/defs.go index 8339660a..aa48a649 100644 --- a/irc/caps/defs.go +++ b/irc/caps/defs.go @@ -7,9 +7,9 @@ package caps const ( // number of recognized capabilities: - numCapabs = 32 + numCapabs = 33 // length of the uint32 array that represents the bitset: - bitsetLen = 1 + bitsetLen = 2 ) const ( @@ -57,6 +57,10 @@ const ( // https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6 Languages Capability = iota + // MessageRedaction is the proposed IRCv3 capability named "draft/message-redaction": + // https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md + MessageRedaction Capability = iota + // Multiline is the proposed IRCv3 capability named "draft/multiline": // https://github.com/ircv3/ircv3-specifications/pull/398 Multiline Capability = iota @@ -156,6 +160,7 @@ var ( "draft/chathistory", "draft/event-playback", "draft/languages", + "draft/message-redaction", "draft/multiline", "draft/persistence", "draft/pre-away", diff --git a/irc/commands.go b/irc/commands.go index d3f828b6..ccbec383 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -301,6 +301,10 @@ func init() { usablePreReg: true, minParams: 0, }, + "REDACT": { + handler: redactHandler, + minParams: 2, + }, "REHASH": { handler: rehashHandler, minParams: 0, diff --git a/irc/handlers.go b/irc/handlers.go index 043519de..b256bb80 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2663,6 +2663,97 @@ fail: return false } +// REDACT [:] +func redactHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool { + target := msg.Params[0] + targetmsgid := msg.Params[1] + //clientOnlyTags := msg.ClientOnlyTags() + var reason string + if len(msg.Params) > 2 { + reason = msg.Params[2] + } + var members []*Client // members of a channel, or both parties of a PM + var canDelete CanDelete + + msgid := utils.GenerateSecretToken() + time := time.Now().UTC().Round(0) + details := client.Details() + isBot := client.HasMode(modes.Bot) + + if target[0] == '#' { + channel := server.channels.Get(target) + if channel == nil { + rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel")) + return false + } + members = channel.Members() + canDelete = deletionPolicy(server, client, target) + } else { + targetClient := server.clients.Get(target) + if targetClient == nil { + rb.Add(nil, server.name, ERR_NOSUCHNICK, client.Nick(), target, "No such nick") + return false + } + members = []*Client{client, targetClient} + canDelete = canDeleteSelf + } + + if canDelete == canDeleteNone { + rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("You are not authorized to delete messages")) + return false + } + accountName := "*" + if canDelete == canDeleteSelf { + accountName = client.AccountName() + if accountName == "*" { + rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("You are not authorized to delete because you are logged out")) + return false + } + } + + err := server.DeleteMessage(target, targetmsgid, accountName) + if err == errNoop { + rb.Add(nil, server.name, "FAIL", "REDACT", "UNKNOWN_MSGID", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("This message does not exist or is too old")) + return false + } else if err != nil { + isOper := client.HasRoleCapabs("history") + if isOper { + rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), fmt.Sprintf(client.t("Error deleting message: %v"), err)) + } else { + rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("Could not delete message")) + } + return false + } + + if target[0] != '#' { + // If this is a PM, we just removed the message from the buffer of the other party; + // now we have to remove it from the buffer of the client who sent the REDACT command + err := server.DeleteMessage(client.Nick(), targetmsgid, accountName) + + if err != nil { + client.server.logger.Error("internal", fmt.Sprintf("Private message %s is not deletable by %s from their own buffer's even though we just deleted it from %s's. This is a bug, please report it in details.", targetmsgid, client.Nick(), target), client.Nick()) + isOper := client.HasRoleCapabs("history") + if isOper { + rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), fmt.Sprintf(client.t("Error deleting message: %v"), err)) + } else { + rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("Error deleting message")) + } + } + } + + for _, member := range members { + for _, session := range member.Sessions() { + if session.capabilities.Has(caps.MessageRedaction) { + session.sendFromClientInternal(false, time, msgid, details.nickMask, details.accountName, isBot, nil, "REDACT", target, targetmsgid, reason) + } else { + // If we wanted to send a fallback to clients which do not support + // draft/message-redaction, we would do it from here. + } + } + } + return false +} + func reportPersistenceStatus(client *Client, rb *ResponseBuffer, broadcast bool) { settings := client.AccountSettings() serverSetting := client.server.Config().Accounts.Multiclient.AlwaysOn diff --git a/irc/help.go b/irc/help.go index 2d972aa8..343bd717 100644 --- a/irc/help.go +++ b/irc/help.go @@ -435,6 +435,12 @@ Replies to a PING. Used to check link connectivity.`, text: `PRIVMSG {,} Sends the text to the given targets as a PRIVMSG.`, + }, + "redact": { + text: `REDACT [] + +Removes the message of the target msgid from the chat history of a channel +or target user.`, }, "relaymsg": { text: `RELAYMSG : diff --git a/irc/histserv.go b/irc/histserv.go index 3416cd6d..f0cb2285 100644 --- a/irc/histserv.go +++ b/irc/histserv.go @@ -15,6 +15,14 @@ import ( "github.com/ergochat/ergo/irc/utils" ) +type CanDelete uint + +const ( + canDeleteAny CanDelete = iota // User is allowed to delete any message (for a given channel/PM) + canDeleteSelf // User is allowed to delete their own messages (ditto) + canDeleteNone // User is not allowed to delete any message (ditto) +) + const ( histservHelp = `HistServ provides commands related to history.` ) @@ -92,33 +100,53 @@ func histservForgetHandler(service *ircService, server *Server, client *Client, service.Notice(rb, fmt.Sprintf(client.t("Enqueued account %s for message deletion"), accountName)) } -func histservDeleteHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { - target, msgid := params[0], params[1] // Fix #1881 2 params are required - - // operators can delete; if individual delete is allowed, a chanop or - // the message author can delete - accountName := "*" - isChanop := false +// Returns: +// +// 1. `canDeleteAny` if the client allowed to delete other users' messages from the target, ie.: +// - the client is a channel operator, or +// - the client is an operator with "history" capability +// +// 2. `canDeleteSelf` if the client is allowed to delete their own messages from the target +// 3. `canDeleteNone` otherwise +func deletionPolicy(server *Server, client *Client, target string) CanDelete { isOper := client.HasRoleCapabs("history") - if !isOper { + if isOper { + return canDeleteAny + } else { if server.Config().History.Retention.AllowIndividualDelete { channel := server.channels.Get(target) if channel != nil && channel.ClientIsAtLeast(client, modes.Operator) { - isChanop = true + return canDeleteAny } else { - accountName = client.AccountName() + return canDeleteSelf } + } else { + return canDeleteNone } } - if !isOper && !isChanop && accountName == "*" { +} + +func histservDeleteHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + target, msgid := params[0], params[1] // Fix #1881 2 params are required + + canDelete := deletionPolicy(server, client, target) + accountName := "*" + if canDelete == canDeleteNone { service.Notice(rb, client.t("Insufficient privileges")) return + } else if canDelete == canDeleteSelf { + accountName = client.AccountName() + if accountName == "*" { + service.Notice(rb, client.t("Insufficient privileges")) + return + } } err := server.DeleteMessage(target, msgid, accountName) if err == nil { service.Notice(rb, client.t("Successfully deleted message")) } else { + isOper := client.HasRoleCapabs("history") if isOper { service.Notice(rb, fmt.Sprintf(client.t("Error deleting message: %v"), err)) } else { diff --git a/traditional.yaml b/traditional.yaml index df5e8cfa..1291771c 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -954,7 +954,8 @@ history: # options to control how messages are stored and deleted: retention: - # allow users to delete their own messages from history? + # allow users to delete their own messages from history, + # and channel operators to delete messages in their channel? allow-individual-delete: false # if persistent history is enabled, create additional index tables,