mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-15 00:19:29 +01:00
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 2182b1da69
.
---------
Co-authored-by: Val Lorentz <progval+git+ergo@progval.net>
This commit is contained in:
parent
bf33fba33a
commit
48f8c341d7
@ -982,7 +982,8 @@ history:
|
|||||||
|
|
||||||
# options to control how messages are stored and deleted:
|
# options to control how messages are stored and deleted:
|
||||||
retention:
|
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
|
allow-individual-delete: false
|
||||||
|
|
||||||
# if persistent history is enabled, create additional index tables,
|
# if persistent history is enabled, create additional index tables,
|
||||||
|
@ -87,6 +87,12 @@ CAPDEFS = [
|
|||||||
url="https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6",
|
url="https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6",
|
||||||
standard="proposed IRCv3",
|
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(
|
CapDef(
|
||||||
identifier="MessageTags",
|
identifier="MessageTags",
|
||||||
name="message-tags",
|
name="message-tags",
|
||||||
|
@ -7,9 +7,9 @@ package caps
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// number of recognized capabilities:
|
// number of recognized capabilities:
|
||||||
numCapabs = 32
|
numCapabs = 33
|
||||||
// length of the uint32 array that represents the bitset:
|
// length of the uint32 array that represents the bitset:
|
||||||
bitsetLen = 1
|
bitsetLen = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -57,6 +57,10 @@ const (
|
|||||||
// https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6
|
// https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6
|
||||||
Languages Capability = iota
|
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":
|
// Multiline is the proposed IRCv3 capability named "draft/multiline":
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/398
|
// https://github.com/ircv3/ircv3-specifications/pull/398
|
||||||
Multiline Capability = iota
|
Multiline Capability = iota
|
||||||
@ -156,6 +160,7 @@ var (
|
|||||||
"draft/chathistory",
|
"draft/chathistory",
|
||||||
"draft/event-playback",
|
"draft/event-playback",
|
||||||
"draft/languages",
|
"draft/languages",
|
||||||
|
"draft/message-redaction",
|
||||||
"draft/multiline",
|
"draft/multiline",
|
||||||
"draft/persistence",
|
"draft/persistence",
|
||||||
"draft/pre-away",
|
"draft/pre-away",
|
||||||
|
@ -301,6 +301,10 @@ func init() {
|
|||||||
usablePreReg: true,
|
usablePreReg: true,
|
||||||
minParams: 0,
|
minParams: 0,
|
||||||
},
|
},
|
||||||
|
"REDACT": {
|
||||||
|
handler: redactHandler,
|
||||||
|
minParams: 2,
|
||||||
|
},
|
||||||
"REHASH": {
|
"REHASH": {
|
||||||
handler: rehashHandler,
|
handler: rehashHandler,
|
||||||
minParams: 0,
|
minParams: 0,
|
||||||
|
@ -2663,6 +2663,97 @@ fail:
|
|||||||
return false
|
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) {
|
func reportPersistenceStatus(client *Client, rb *ResponseBuffer, broadcast bool) {
|
||||||
settings := client.AccountSettings()
|
settings := client.AccountSettings()
|
||||||
serverSetting := client.server.Config().Accounts.Multiclient.AlwaysOn
|
serverSetting := client.server.Config().Accounts.Multiclient.AlwaysOn
|
||||||
|
@ -435,6 +435,12 @@ Replies to a PING. Used to check link connectivity.`,
|
|||||||
text: `PRIVMSG <target>{,<target>} <text to be sent>
|
text: `PRIVMSG <target>{,<target>} <text to be sent>
|
||||||
|
|
||||||
Sends the text to the given targets as a PRIVMSG.`,
|
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": {
|
"relaymsg": {
|
||||||
text: `RELAYMSG <channel> <spoofed nick> :<message>
|
text: `RELAYMSG <channel> <spoofed nick> :<message>
|
||||||
|
@ -15,6 +15,14 @@ import (
|
|||||||
"github.com/ergochat/ergo/irc/utils"
|
"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 (
|
const (
|
||||||
histservHelp = `HistServ provides commands related to history.`
|
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))
|
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) {
|
// Returns:
|
||||||
target, msgid := params[0], params[1] // Fix #1881 2 params are required
|
//
|
||||||
|
// 1. `canDeleteAny` if the client allowed to delete other users' messages from the target, ie.:
|
||||||
// operators can delete; if individual delete is allowed, a chanop or
|
// - the client is a channel operator, or
|
||||||
// the message author can delete
|
// - the client is an operator with "history" capability
|
||||||
accountName := "*"
|
//
|
||||||
isChanop := false
|
// 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")
|
isOper := client.HasRoleCapabs("history")
|
||||||
if !isOper {
|
if isOper {
|
||||||
|
return canDeleteAny
|
||||||
|
} else {
|
||||||
if server.Config().History.Retention.AllowIndividualDelete {
|
if server.Config().History.Retention.AllowIndividualDelete {
|
||||||
channel := server.channels.Get(target)
|
channel := server.channels.Get(target)
|
||||||
if channel != nil && channel.ClientIsAtLeast(client, modes.Operator) {
|
if channel != nil && channel.ClientIsAtLeast(client, modes.Operator) {
|
||||||
isChanop = true
|
return canDeleteAny
|
||||||
} else {
|
} 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"))
|
service.Notice(rb, client.t("Insufficient privileges"))
|
||||||
return
|
return
|
||||||
|
} else if canDelete == canDeleteSelf {
|
||||||
|
accountName = client.AccountName()
|
||||||
|
if accountName == "*" {
|
||||||
|
service.Notice(rb, client.t("Insufficient privileges"))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := server.DeleteMessage(target, msgid, accountName)
|
err := server.DeleteMessage(target, msgid, accountName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
service.Notice(rb, client.t("Successfully deleted message"))
|
service.Notice(rb, client.t("Successfully deleted message"))
|
||||||
} else {
|
} else {
|
||||||
|
isOper := client.HasRoleCapabs("history")
|
||||||
if isOper {
|
if isOper {
|
||||||
service.Notice(rb, fmt.Sprintf(client.t("Error deleting message: %v"), err))
|
service.Notice(rb, fmt.Sprintf(client.t("Error deleting message: %v"), err))
|
||||||
} else {
|
} else {
|
||||||
|
@ -954,7 +954,8 @@ history:
|
|||||||
|
|
||||||
# options to control how messages are stored and deleted:
|
# options to control how messages are stored and deleted:
|
||||||
retention:
|
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
|
allow-individual-delete: false
|
||||||
|
|
||||||
# if persistent history is enabled, create additional index tables,
|
# if persistent history is enabled, create additional index tables,
|
||||||
|
Loading…
Reference in New Issue
Block a user