Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

)

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
Comment on lines +2729 to +2730
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

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