Skip to content

Commit 6ffbb2f

Browse files
progvalk4bek4be
authored andcommitted
redact: Initial commit
1 parent 3a23d57 commit 6ffbb2f

File tree

1 file changed

+268
-0
lines changed

1 file changed

+268
-0
lines changed

files/redact.c

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/*
2+
** Copyright (C) 2023 Valentin Lorentz
3+
** License: GPLv3 https://www.gnu.org/licenses/gpl-3.0.html
4+
*/
5+
6+
/*** <<<MODULE MANAGER START>>>
7+
module
8+
{
9+
documentation "Implements the draft IRCv3 message-redaction specification https://github.com/ircv3/ircv3-specifications/pull/524"
10+
troubleshooting "In case of problems, contact val on irc.unrealircd.org.";
11+
min-unrealircd-version "6.*";
12+
post-install-text {
13+
"The module is installed. Now all you need to do is add a loadmodule line:";
14+
"loadmodule \"third/redact\";";
15+
"And /REHASH the IRCd.";
16+
"The module may be additionaly configured to change the defaults.";
17+
"See documentation for help.";
18+
"Please note that the implemented feature is still \"Work In Progress\".";
19+
}
20+
}
21+
*** <<<MODULE MANAGER END>>>
22+
*/
23+
24+
#include "unrealircd.h"
25+
26+
27+
ModuleHeader MOD_HEADER = {
28+
"third/redact",
29+
"6.0",
30+
"draft/message-redaction cap",
31+
"val",
32+
"unrealircd-6"
33+
};
34+
35+
/* Forward declarations */
36+
CMD_FUNC(cmd_redact);
37+
38+
/* Global variables */
39+
long CAP_MESSAGE_REDACTION = 0L;
40+
bool sender_can_redact = 0;
41+
char *chan_access_pattern = "";
42+
43+
44+
int redact_config_run(ConfigFile *cf, ConfigEntry *ce, int type) {
45+
int errors = 0;
46+
ConfigEntry *cep, *cep2;
47+
48+
if (type != CONFIG_SET)
49+
return 0;
50+
51+
if (!ce || !ce->name || strcmp(ce->name, "redacters"))
52+
return 0;
53+
54+
bool owners_can_redact = 0;
55+
bool admins_can_redact = 0;
56+
bool ops_can_redact = 0;
57+
bool halfops_can_redact = 0;
58+
59+
for (cep = ce->items; cep; cep = cep->next)
60+
{
61+
if (!cep->name)
62+
{
63+
config_error("%s:%i: blank set::redacters item",
64+
cep->file->filename, cep->line_number);
65+
errors++;
66+
continue;
67+
}
68+
else if (!strcmp(cep->name, "owner"))
69+
owners_can_redact = 1;
70+
else if (!strcmp(cep->name, "admin"))
71+
admins_can_redact = 1;
72+
else if (!strcmp(cep->name, "op"))
73+
ops_can_redact = 1;
74+
else if (!strcmp(cep->name, "halfop"))
75+
halfops_can_redact = 1;
76+
else if (!strcmp(cep->name, "sender"))
77+
sender_can_redact = 1;
78+
else {
79+
/* Should have been caught in redact_config_test */
80+
continue;
81+
}
82+
}
83+
84+
if (halfops_can_redact)
85+
chan_access_pattern = "hoaq";
86+
else if (ops_can_redact)
87+
chan_access_pattern = "oaq";
88+
else if (admins_can_redact)
89+
chan_access_pattern = "aq";
90+
else if (owners_can_redact)
91+
chan_access_pattern = "q";
92+
else
93+
chan_access_pattern = "";
94+
95+
return 1;
96+
}
97+
98+
int redact_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) {
99+
int errors = 0;
100+
ConfigEntry *cep, *cep2;
101+
102+
if (type != CONFIG_SET)
103+
return 0;
104+
105+
if (!ce || !ce->name || strcmp(ce->name, "redacters"))
106+
return 0;
107+
108+
for (cep = ce->items; cep; cep = cep->next)
109+
{
110+
if (!cep->name)
111+
{
112+
config_error("%s:%i: blank set::redacters item",
113+
cep->file->filename, cep->line_number);
114+
errors++;
115+
continue;
116+
}
117+
else if (!strcmp(cep->name, "owner") && !strcmp(cep->name, "admin") && !strcmp(cep->name, "op") && !strcmp(cep->name, "halfop") && !strcmp(cep->name, "sender")) {
118+
config_error("%s:%i: invalid set::redacters item: %s",
119+
cep->file->filename, cep->line_number, cep->name);
120+
errors++;
121+
continue;
122+
}
123+
}
124+
125+
*errs = errors;
126+
return errors ? -1 : 1;
127+
}
128+
129+
MOD_INIT()
130+
{
131+
ClientCapabilityInfo c;
132+
133+
/* Will be set when reading the config */
134+
chan_access_pattern = "";
135+
sender_can_redact = 0;
136+
137+
CommandAdd(modinfo->handle, "REDACT", cmd_redact, MAXPARA, CMD_USER|CMD_SERVER);
138+
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, redact_config_run);
139+
140+
memset(&c, 0, sizeof(c));
141+
c.name = "draft/message-redaction";
142+
ClientCapabilityAdd(modinfo->handle, &c, &CAP_MESSAGE_REDACTION);
143+
return MOD_SUCCESS;
144+
}
145+
146+
MOD_LOAD()
147+
{
148+
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, redact_config_run);
149+
return MOD_SUCCESS;
150+
}
151+
152+
MOD_TEST()
153+
{
154+
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, redact_config_test);
155+
return MOD_SUCCESS;
156+
}
157+
158+
MOD_UNLOAD()
159+
{
160+
return MOD_SUCCESS;
161+
}
162+
163+
CMD_FUNC(cmd_redact)
164+
{
165+
HistoryFilter *filter = NULL;
166+
HistoryResult *r = NULL;
167+
Channel *channel;
168+
char *account;
169+
char *error;
170+
int deleted, rejected_deletes;
171+
172+
if ((parc < 2) || BadPtr(parv[1]))
173+
{
174+
sendnumeric(client, ERR_NEEDMOREPARAMS, "REDACT");
175+
return;
176+
}
177+
178+
channel = find_channel(parv[1]);
179+
if (!channel)
180+
{
181+
sendto_one(client, NULL, ":%s FAIL REDACT INVALID_TARGET %s :Chat history is not enabled in PM",
182+
me.name, parv[1]);
183+
return;
184+
}
185+
186+
int is_oper = ValidatePermissionsForPath("chat:redact",client,NULL,NULL,NULL);
187+
bool check_sender = 0;
188+
189+
if (MyUser(client) && !is_oper && (BadPtr(chan_access_pattern) || !check_channel_access(client, channel, chan_access_pattern)))
190+
{
191+
if (sender_can_redact)
192+
/* Provisionally allow the command, until we retrieved the message
193+
* they are trying to delete (because we need to check they are its
194+
* sender */
195+
check_sender = 1;
196+
else {
197+
error = "sender can't redact, and neither oper nor chanop";
198+
goto unauthorized;
199+
}
200+
}
201+
202+
203+
filter = safe_alloc(sizeof(HistoryFilter));
204+
filter->cmd = HFC_AROUND;
205+
filter->msgid_a = strdup(parv[2]);
206+
filter->limit = 1;
207+
208+
if (check_sender) {
209+
if (IsLoggedIn(client))
210+
filter->account = strdup(client->user->account);
211+
else {
212+
error = "neither oper/chanop nor logged in";
213+
goto unauthorized;
214+
}
215+
}
216+
217+
deleted = history_delete(channel->name, filter, &rejected_deletes);
218+
219+
if (deleted > 1) {
220+
sendto_one(client, NULL, ":%s FAIL REDACT UNKNOWN_ERROR %s :history_delete found more than one result",
221+
me.name, parv[1]);
222+
goto end;
223+
}
224+
else if (rejected_deletes > 1) {
225+
sendto_one(client, NULL, ":%s FAIL REDACT UNKNOWN_ERROR %s :history_delete rejected more than one result",
226+
me.name, parv[1]);
227+
goto end;
228+
}
229+
else if (deleted && rejected_deletes) {
230+
sendto_one(client, NULL, ":%s FAIL REDACT UNKNOWN_ERROR %s %s :history_delete both deleted and rejected",
231+
me.name, parv[1], parv[2]);
232+
goto end;
233+
}
234+
else if (rejected_deletes) {
235+
error = "not sender";
236+
goto unauthorized;
237+
}
238+
else if (!deleted) {
239+
sendto_one(client, NULL, ":%s FAIL REDACT UNKNOWN_MSGID %s %s :This message does not exist or is too old",
240+
me.name, parv[1], parv[2]);
241+
goto end;
242+
}
243+
244+
if (parc >= 3) {
245+
/* Has a reason */
246+
sendto_channel(channel, client, /* skip */ NULL, /* member_modes */ NULL,
247+
CAP_MESSAGE_REDACTION, SEND_ALL, /* mtags */ NULL,
248+
":%s REDACT %s %s :%s",
249+
client->name, parv[1], parv[2], parv[3]);
250+
}
251+
else {
252+
sendto_channel(channel, client, /* skip */ NULL, /* member_modes */ NULL,
253+
CAP_MESSAGE_REDACTION, SEND_ALL, /* mtags */ NULL,
254+
":%s REDACT %s %s",
255+
client->name, parv[1], parv[2]);
256+
}
257+
258+
goto end;
259+
260+
unauthorized:
261+
sendto_one(client, NULL, ":%s FAIL REDACT REDACT_FORBIDDEN %s %s :Your are not authorized to redact messages in %s: %s",
262+
me.name, parv[1], parv[2], parv[1], error);
263+
end:
264+
if (filter)
265+
free_history_filter(filter);
266+
if (r)
267+
free_history_result(r);
268+
}

0 commit comments

Comments
 (0)