Browse Source

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>
tags/v2.12.0-rc1
Val Lorentz 11 months ago
parent
commit
48f8c341d7
No account linked to committer's email address
8 changed files with 157 additions and 15 deletions
  1. 2
    1
      default.yaml
  2. 6
    0
      gencapdefs.py
  3. 7
    2
      irc/caps/defs.go
  4. 4
    0
      irc/commands.go
  5. 91
    0
      irc/handlers.go
  6. 6
    0
      irc/help.go
  7. 39
    11
      irc/histserv.go
  8. 2
    1
      traditional.yaml

+ 2
- 1
default.yaml View File

@@ -982,7 +982,8 @@ history:
982 982
 
983 983
     # options to control how messages are stored and deleted:
984 984
     retention:
985
-        # allow users to delete their own messages from history?
985
+        # allow users to delete their own messages from history,
986
+        # and channel operators to delete messages in their channel?
986 987
         allow-individual-delete: false
987 988
 
988 989
         # if persistent history is enabled, create additional index tables,

+ 6
- 0
gencapdefs.py View File

@@ -87,6 +87,12 @@ CAPDEFS = [
87 87
         url="https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6",
88 88
         standard="proposed IRCv3",
89 89
     ),
90
+    CapDef(
91
+        identifier="MessageRedaction",
92
+        name="draft/message-redaction",
93
+        url="https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md",
94
+        standard="proposed IRCv3",
95
+    ),
90 96
     CapDef(
91 97
         identifier="MessageTags",
92 98
         name="message-tags",

+ 7
- 2
irc/caps/defs.go View File

@@ -7,9 +7,9 @@ package caps
7 7
 
8 8
 const (
9 9
 	// number of recognized capabilities:
10
-	numCapabs = 32
10
+	numCapabs = 33
11 11
 	// length of the uint32 array that represents the bitset:
12
-	bitsetLen = 1
12
+	bitsetLen = 2
13 13
 )
14 14
 
15 15
 const (
@@ -57,6 +57,10 @@ const (
57 57
 	// https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6
58 58
 	Languages Capability = iota
59 59
 
60
+	// MessageRedaction is the proposed IRCv3 capability named "draft/message-redaction":
61
+	// https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md
62
+	MessageRedaction Capability = iota
63
+
60 64
 	// Multiline is the proposed IRCv3 capability named "draft/multiline":
61 65
 	// https://github.com/ircv3/ircv3-specifications/pull/398
62 66
 	Multiline Capability = iota
@@ -156,6 +160,7 @@ var (
156 160
 		"draft/chathistory",
157 161
 		"draft/event-playback",
158 162
 		"draft/languages",
163
+		"draft/message-redaction",
159 164
 		"draft/multiline",
160 165
 		"draft/persistence",
161 166
 		"draft/pre-away",

+ 4
- 0
irc/commands.go View File

@@ -301,6 +301,10 @@ func init() {
301 301
 			usablePreReg: true,
302 302
 			minParams:    0,
303 303
 		},
304
+		"REDACT": {
305
+			handler:   redactHandler,
306
+			minParams: 2,
307
+		},
304 308
 		"REHASH": {
305 309
 			handler:   rehashHandler,
306 310
 			minParams: 0,

+ 91
- 0
irc/handlers.go View File

@@ -2663,6 +2663,97 @@ fail:
2663 2663
 	return false
2664 2664
 }
2665 2665
 
2666
+// REDACT <target> <targetmsgid> [:<reason>]
2667
+func redactHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
2668
+	target := msg.Params[0]
2669
+	targetmsgid := msg.Params[1]
2670
+	//clientOnlyTags := msg.ClientOnlyTags()
2671
+	var reason string
2672
+	if len(msg.Params) > 2 {
2673
+		reason = msg.Params[2]
2674
+	}
2675
+	var members []*Client // members of a channel, or both parties of a PM
2676
+	var canDelete CanDelete
2677
+
2678
+	msgid := utils.GenerateSecretToken()
2679
+	time := time.Now().UTC().Round(0)
2680
+	details := client.Details()
2681
+	isBot := client.HasMode(modes.Bot)
2682
+
2683
+	if target[0] == '#' {
2684
+		channel := server.channels.Get(target)
2685
+		if channel == nil {
2686
+			rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel"))
2687
+			return false
2688
+		}
2689
+		members = channel.Members()
2690
+		canDelete = deletionPolicy(server, client, target)
2691
+	} else {
2692
+		targetClient := server.clients.Get(target)
2693
+		if targetClient == nil {
2694
+			rb.Add(nil, server.name, ERR_NOSUCHNICK, client.Nick(), target, "No such nick")
2695
+			return false
2696
+		}
2697
+		members = []*Client{client, targetClient}
2698
+		canDelete = canDeleteSelf
2699
+	}
2700
+
2701
+	if canDelete == canDeleteNone {
2702
+		rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("You are not authorized to delete messages"))
2703
+		return false
2704
+	}
2705
+	accountName := "*"
2706
+	if canDelete == canDeleteSelf {
2707
+		accountName = client.AccountName()
2708
+		if accountName == "*" {
2709
+			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"))
2710
+			return false
2711
+		}
2712
+	}
2713
+
2714
+	err := server.DeleteMessage(target, targetmsgid, accountName)
2715
+	if err == errNoop {
2716
+		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"))
2717
+		return false
2718
+	} else if err != nil {
2719
+		isOper := client.HasRoleCapabs("history")
2720
+		if isOper {
2721
+			rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), fmt.Sprintf(client.t("Error deleting message: %v"), err))
2722
+		} else {
2723
+			rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("Could not delete message"))
2724
+		}
2725
+		return false
2726
+	}
2727
+
2728
+	if target[0] != '#' {
2729
+		// If this is a PM, we just removed the message from the buffer of the other party;
2730
+		// now we have to remove it from the buffer of the client who sent the REDACT command
2731
+		err := server.DeleteMessage(client.Nick(), targetmsgid, accountName)
2732
+
2733
+		if err != nil {
2734
+			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())
2735
+			isOper := client.HasRoleCapabs("history")
2736
+			if isOper {
2737
+				rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), fmt.Sprintf(client.t("Error deleting message: %v"), err))
2738
+			} else {
2739
+				rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), client.t("Error deleting message"))
2740
+			}
2741
+		}
2742
+	}
2743
+
2744
+	for _, member := range members {
2745
+		for _, session := range member.Sessions() {
2746
+			if session.capabilities.Has(caps.MessageRedaction) {
2747
+				session.sendFromClientInternal(false, time, msgid, details.nickMask, details.accountName, isBot, nil, "REDACT", target, targetmsgid, reason)
2748
+			} else {
2749
+				// If we wanted to send a fallback to clients which do not support
2750
+				// draft/message-redaction, we would do it from here.
2751
+			}
2752
+		}
2753
+	}
2754
+	return false
2755
+}
2756
+
2666 2757
 func reportPersistenceStatus(client *Client, rb *ResponseBuffer, broadcast bool) {
2667 2758
 	settings := client.AccountSettings()
2668 2759
 	serverSetting := client.server.Config().Accounts.Multiclient.AlwaysOn

+ 6
- 0
irc/help.go View File

@@ -435,6 +435,12 @@ Replies to a PING. Used to check link connectivity.`,
435 435
 		text: `PRIVMSG <target>{,<target>} <text to be sent>
436 436
 
437 437
 Sends the text to the given targets as a PRIVMSG.`,
438
+	},
439
+	"redact": {
440
+		text: `REDACT <target> <targetmsgid> [<reason>]
441
+
442
+Removes the message of the target msgid from the chat history of a channel
443
+or target user.`,
438 444
 	},
439 445
 	"relaymsg": {
440 446
 		text: `RELAYMSG <channel> <spoofed nick> :<message>

+ 39
- 11
irc/histserv.go View File

@@ -15,6 +15,14 @@ import (
15 15
 	"github.com/ergochat/ergo/irc/utils"
16 16
 )
17 17
 
18
+type CanDelete uint
19
+
20
+const (
21
+	canDeleteAny  CanDelete = iota // User is allowed to delete any message (for a given channel/PM)
22
+	canDeleteSelf                  // User is allowed to delete their own messages (ditto)
23
+	canDeleteNone                  // User is not allowed to delete any message (ditto)
24
+)
25
+
18 26
 const (
19 27
 	histservHelp = `HistServ provides commands related to history.`
20 28
 )
@@ -92,33 +100,53 @@ func histservForgetHandler(service *ircService, server *Server, client *Client,
92 100
 	service.Notice(rb, fmt.Sprintf(client.t("Enqueued account %s for message deletion"), accountName))
93 101
 }
94 102
 
95
-func histservDeleteHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
96
-	target, msgid := params[0], params[1] // Fix #1881 2 params are required
97
-
98
-	// operators can delete; if individual delete is allowed, a chanop or
99
-	// the message author can delete
100
-	accountName := "*"
101
-	isChanop := false
103
+// Returns:
104
+//
105
+// 1. `canDeleteAny` if the client allowed to delete other users' messages from the target, ie.:
106
+//   - the client is a channel operator, or
107
+//   - the client is an operator with "history" capability
108
+//
109
+// 2. `canDeleteSelf` if the client is allowed to delete their own messages from the target
110
+// 3. `canDeleteNone` otherwise
111
+func deletionPolicy(server *Server, client *Client, target string) CanDelete {
102 112
 	isOper := client.HasRoleCapabs("history")
103
-	if !isOper {
113
+	if isOper {
114
+		return canDeleteAny
115
+	} else {
104 116
 		if server.Config().History.Retention.AllowIndividualDelete {
105 117
 			channel := server.channels.Get(target)
106 118
 			if channel != nil && channel.ClientIsAtLeast(client, modes.Operator) {
107
-				isChanop = true
119
+				return canDeleteAny
108 120
 			} else {
109
-				accountName = client.AccountName()
121
+				return canDeleteSelf
110 122
 			}
123
+		} else {
124
+			return canDeleteNone
111 125
 		}
112 126
 	}
113
-	if !isOper && !isChanop && accountName == "*" {
127
+}
128
+
129
+func histservDeleteHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
130
+	target, msgid := params[0], params[1] // Fix #1881 2 params are required
131
+
132
+	canDelete := deletionPolicy(server, client, target)
133
+	accountName := "*"
134
+	if canDelete == canDeleteNone {
114 135
 		service.Notice(rb, client.t("Insufficient privileges"))
115 136
 		return
137
+	} else if canDelete == canDeleteSelf {
138
+		accountName = client.AccountName()
139
+		if accountName == "*" {
140
+			service.Notice(rb, client.t("Insufficient privileges"))
141
+			return
142
+		}
116 143
 	}
117 144
 
118 145
 	err := server.DeleteMessage(target, msgid, accountName)
119 146
 	if err == nil {
120 147
 		service.Notice(rb, client.t("Successfully deleted message"))
121 148
 	} else {
149
+		isOper := client.HasRoleCapabs("history")
122 150
 		if isOper {
123 151
 			service.Notice(rb, fmt.Sprintf(client.t("Error deleting message: %v"), err))
124 152
 		} else {

+ 2
- 1
traditional.yaml View File

@@ -954,7 +954,8 @@ history:
954 954
 
955 955
     # options to control how messages are stored and deleted:
956 956
     retention:
957
-        # allow users to delete their own messages from history?
957
+        # allow users to delete their own messages from history,
958
+        # and channel operators to delete messages in their channel?
958 959
         allow-individual-delete: false
959 960
 
960 961
         # if persistent history is enabled, create additional index tables,

Loading…
Cancel
Save