Skip to content

Commit

Permalink
Implement draft/message-redaction (#2065)
Browse files Browse the repository at this point in the history
* 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 2182b1d.

---------

Co-authored-by: Val Lorentz <[email protected]>
  • Loading branch information
progval and Val Lorentz authored May 31, 2023
1 parent bf33fba commit 48f8c34
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 15 deletions.
3 changes: 2 additions & 1 deletion default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions gencapdefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@
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",
Expand Down
9 changes: 7 additions & 2 deletions irc/caps/defs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -156,6 +160,7 @@ var (
"draft/chathistory",
"draft/event-playback",
"draft/languages",
"draft/message-redaction",
"draft/multiline",
"draft/persistence",
"draft/pre-away",
Expand Down
4 changes: 4 additions & 0 deletions irc/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ func init() {
usablePreReg: true,
minParams: 0,
},
"REDACT": {
handler: redactHandler,
minParams: 2,
},
"REHASH": {
handler: rehashHandler,
minParams: 0,
Expand Down
91 changes: 91 additions & 0 deletions irc/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2663,6 +2663,97 @@ fail:
return false
}

// REDACT <target> <targetmsgid> [:<reason>]
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
Expand Down
6 changes: 6 additions & 0 deletions irc/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,12 @@ Replies to a PING. Used to check link connectivity.`,
text: `PRIVMSG <target>{,<target>} <text to be sent>
Sends the text to the given targets as a PRIVMSG.`,
},
"redact": {
text: `REDACT <target> <targetmsgid> [<reason>]
Removes the message of the target msgid from the chat history of a channel
or target user.`,
},
"relaymsg": {
text: `RELAYMSG <channel> <spoofed nick> :<message>
Expand Down
50 changes: 39 additions & 11 deletions irc/histserv.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.`
)
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion traditional.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 48f8c34

Please sign in to comment.