unrealircd- supernets unrealircd source & configuration |
git clone git://git.acid.vegas/unrealircd.git |
Log | Files | Refs | Archive | README | LICENSE |
chathistory.c (10180B)
1 /* src/modules/chathistory.c - IRCv3 CHATHISTORY command. 2 * (C) Copyright 2021 Bram Matthys (Syzop) and the UnrealIRCd team 3 * License: GPLv2 or later 4 * 5 * This implements the "CHATHISTORY" command, the CAP and 005 token. 6 * https://ircv3.net/specs/extensions/chathistory 7 */ 8 #include "unrealircd.h" 9 10 ModuleHeader MOD_HEADER 11 = { 12 "chathistory", 13 "1.0", 14 "IRCv3 CHATHISTORY command", 15 "UnrealIRCd Team", 16 "unrealircd-6", 17 }; 18 19 /* Structs */ 20 typedef struct ChatHistoryTarget ChatHistoryTarget; 21 struct ChatHistoryTarget { 22 ChatHistoryTarget *prev, *next; 23 char *datetime; 24 char *object; 25 }; 26 27 /* Forward declarations */ 28 CMD_FUNC(cmd_chathistory); 29 30 /* Global variables */ 31 long CAP_CHATHISTORY = 0L; 32 33 #define CHATHISTORY_LIMIT 50 34 35 MOD_INIT() 36 { 37 ClientCapabilityInfo c; 38 39 MARK_AS_OFFICIAL_MODULE(modinfo); 40 CommandAdd(modinfo->handle, "CHATHISTORY", cmd_chathistory, MAXPARA, CMD_USER); 41 42 memset(&c, 0, sizeof(c)); 43 c.name = "draft/chathistory"; 44 ClientCapabilityAdd(modinfo->handle, &c, &CAP_CHATHISTORY); 45 return MOD_SUCCESS; 46 } 47 48 MOD_LOAD() 49 { 50 ISupportSetFmt(modinfo->handle, "CHATHISTORY", "%d", CHATHISTORY_LIMIT); 51 return MOD_SUCCESS; 52 } 53 54 MOD_UNLOAD() 55 { 56 return MOD_SUCCESS; 57 } 58 59 int chathistory_token(const char *str, char *token, char **store) 60 { 61 char request[BUFSIZE]; 62 char *p; 63 64 strlcpy(request, str, sizeof(request)); 65 66 p = strchr(request, '='); 67 if (!p) 68 return 0; 69 *p = '\0'; // frag 70 if (!strcmp(request, token)) 71 { 72 *p = '='; // restore 73 *store = strdup(p + 1); // can be \0 74 return 1; 75 } 76 *p = '='; // restore 77 return 0; 78 } 79 80 static void add_chathistory_target_list(ChatHistoryTarget *new, ChatHistoryTarget **list) 81 { 82 ChatHistoryTarget *x, *last = NULL; 83 84 if (!*list) 85 { 86 /* We are the only item. Easy. */ 87 *list = new; 88 return; 89 } 90 91 for (x = *list; x; x = x->next) 92 { 93 last = x; 94 if (strcmp(new->datetime, x->datetime) >= 0) 95 break; 96 } 97 98 if (x) 99 { 100 if (x->prev) 101 { 102 /* We will insert ourselves just before this item */ 103 new->prev = x->prev; 104 new->next = x; 105 x->prev->next = new; 106 x->prev = new; 107 } else { 108 /* We are the new head */ 109 *list = new; 110 new->next = x; 111 x->prev = new; 112 } 113 } else 114 { 115 /* We are the last item */ 116 last->next = new; 117 new->prev = last; 118 } 119 } 120 121 static void add_chathistory_target(ChatHistoryTarget **list, HistoryResult *r) 122 { 123 MessageTag *m; 124 time_t ts; 125 char *datetime; 126 ChatHistoryTarget *e; 127 128 if (!r->log || !((m = find_mtag(r->log->mtags, "time"))) || !m->value) 129 return; 130 datetime = m->value; 131 132 e = safe_alloc(sizeof(ChatHistoryTarget)); 133 safe_strdup(e->datetime, datetime); 134 safe_strdup(e->object, r->object); 135 add_chathistory_target_list(e, list); 136 } 137 138 static void chathistory_targets_send_line(Client *client, ChatHistoryTarget *r, char *batchid) 139 { 140 MessageTag *mtags = NULL; 141 MessageTag *m; 142 143 if (!BadPtr(batchid)) 144 { 145 mtags = safe_alloc(sizeof(MessageTag)); 146 mtags->name = strdup("batch"); 147 mtags->value = strdup(batchid); 148 } 149 150 sendto_one(client, mtags, ":%s CHATHISTORY TARGETS %s %s", 151 me.name, r->object, r->datetime); 152 153 if (mtags) 154 free_message_tags(mtags); 155 } 156 157 void chathistory_targets(Client *client, HistoryFilter *filter, int limit) 158 { 159 Membership *mp; 160 HistoryResult *r; 161 char batch[BATCHLEN+1]; 162 int sent = 0; 163 ChatHistoryTarget *targets = NULL, *targets_next; 164 165 /* 1. Grab all information we need */ 166 167 filter->cmd = HFC_BEFORE; 168 if (strcmp(filter->timestamp_a, filter->timestamp_b) < 0) 169 { 170 /* Swap if needed */ 171 char *swap = filter->timestamp_a; 172 filter->timestamp_a = filter->timestamp_b; 173 filter->timestamp_b = swap; 174 } 175 filter->limit = 1; 176 177 for (mp = client->user->channel; mp; mp = mp->next) 178 { 179 Channel *channel = mp->channel; 180 r = history_request(channel->name, filter); 181 if (r) 182 { 183 add_chathistory_target(&targets, r); 184 free_history_result(r); 185 } 186 } 187 188 /* 2. Now send it to the client */ 189 190 batch[0] = '\0'; 191 if (HasCapability(client, "batch")) 192 { 193 /* Start a new batch */ 194 generate_batch_id(batch); 195 sendto_one(client, NULL, ":%s BATCH +%s draft/chathistory-targets", me.name, batch); 196 } 197 198 for (; targets; targets = targets_next) 199 { 200 targets_next = targets->next; 201 if (++sent < limit) 202 chathistory_targets_send_line(client, targets, batch); 203 safe_free(targets->datetime); 204 safe_free(targets->object); 205 safe_free(targets); 206 } 207 208 /* End of batch */ 209 if (*batch) 210 sendto_one(client, NULL, ":%s BATCH -%s", me.name, batch); 211 } 212 213 void send_empty_batch(Client *client, const char *target) 214 { 215 char batch[BATCHLEN+1]; 216 217 if (HasCapability(client, "batch")) 218 { 219 generate_batch_id(batch); 220 sendto_one(client, NULL, ":%s BATCH +%s chathistory %s", me.name, batch, target); 221 sendto_one(client, NULL, ":%s BATCH -%s", me.name, batch); 222 } 223 } 224 225 CMD_FUNC(cmd_chathistory) 226 { 227 HistoryFilter *filter = NULL; 228 HistoryResult *r = NULL; 229 Channel *channel; 230 231 memset(&filter, 0, sizeof(filter)); 232 233 /* This command is only for local users */ 234 if (!MyUser(client)) 235 return; 236 237 if ((parc < 5) || BadPtr(parv[4])) 238 { 239 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS :Insufficient parameters", me.name); 240 return; 241 } 242 243 if (!HasCapability(client, "server-time")) 244 { 245 sendnotice(client, "Your IRC client does not support the 'server-time' capability"); 246 sendnotice(client, "https://ircv3.net/specs/extensions/server-time"); 247 sendnotice(client, "History request refused."); 248 return; 249 } 250 251 if (!strcasecmp(parv[1], "TARGETS")) 252 { 253 Membership *mp; 254 int limit; 255 256 filter = safe_alloc(sizeof(HistoryFilter)); 257 /* Below this point, instead of 'return', use 'goto end' */ 258 259 if (!chathistory_token(parv[2], "timestamp", &filter->timestamp_a)) 260 { 261 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx", 262 me.name, parv[1], parv[3]); 263 goto end; 264 } 265 if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_b)) 266 { 267 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx", 268 me.name, parv[1], parv[4]); 269 goto end; 270 } 271 limit = atoi(parv[4]); 272 chathistory_targets(client, filter, limit); 273 goto end; 274 } 275 276 /* We don't support retrieving chathistory for PM's. Send empty response/batch, similar to channels without +H. */ 277 if (parv[2][0] != '#') 278 { 279 send_empty_batch(client, parv[2]); 280 return; 281 } 282 283 channel = find_channel(parv[2]); 284 if (!channel) 285 { 286 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_TARGET %s %s :Messages could not be retrieved, not an existing channel", 287 me.name, parv[1], parv[2]); 288 return; 289 } 290 291 if (!IsMember(client, channel)) 292 { 293 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_TARGET %s %s :Messages could not be retrieved, you are not a member", 294 me.name, parv[1], parv[2]); 295 return; 296 } 297 298 /* Channel is not +H? Send empty response/batch (as per IRCv3 discussion) */ 299 if (!has_channel_mode(channel, 'H')) 300 { 301 send_empty_batch(client, channel->name); 302 return; 303 } 304 305 filter = safe_alloc(sizeof(HistoryFilter)); 306 /* Below this point, instead of 'return', use 'goto end', which takes care of the freeing of 'filter' and 'history' */ 307 308 if (!strcasecmp(parv[1], "BEFORE")) 309 { 310 filter->cmd = HFC_BEFORE; 311 if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) && 312 !chathistory_token(parv[3], "msgid", &filter->msgid_a)) 313 { 314 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx", 315 me.name, parv[1], parv[3]); 316 goto end; 317 } 318 filter->limit = atoi(parv[4]); 319 } else 320 if (!strcasecmp(parv[1], "AFTER")) 321 { 322 filter->cmd = HFC_AFTER; 323 if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) && 324 !chathistory_token(parv[3], "msgid", &filter->msgid_a)) 325 { 326 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx", 327 me.name, parv[1], parv[3]); 328 goto end; 329 } 330 filter->limit = atoi(parv[4]); 331 } else 332 if (!strcasecmp(parv[1], "LATEST")) 333 { 334 filter->cmd = HFC_LATEST; 335 if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) && 336 !chathistory_token(parv[3], "msgid", &filter->msgid_a) && 337 strcmp(parv[3], "*")) 338 { 339 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx or *", 340 me.name, parv[1], parv[3]); 341 goto end; 342 } 343 filter->limit = atoi(parv[4]); 344 } else 345 if (!strcasecmp(parv[1], "AROUND")) 346 { 347 filter->cmd = HFC_AROUND; 348 if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) && 349 !chathistory_token(parv[3], "msgid", &filter->msgid_a)) 350 { 351 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx", 352 me.name, parv[1], parv[3]); 353 goto end; 354 } 355 filter->limit = atoi(parv[4]); 356 } else 357 if (!strcasecmp(parv[1], "BETWEEN")) 358 { 359 filter->cmd = HFC_BETWEEN; 360 if (BadPtr(parv[5])) 361 { 362 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s :Insufficient parameters", parv[1], me.name); 363 goto end; 364 } 365 if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) && 366 !chathistory_token(parv[3], "msgid", &filter->msgid_a)) 367 { 368 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx", 369 me.name, parv[1], parv[3]); 370 goto end; 371 } 372 if (!chathistory_token(parv[4], "timestamp", &filter->timestamp_b) && 373 !chathistory_token(parv[4], "msgid", &filter->msgid_b)) 374 { 375 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx", 376 me.name, parv[1], parv[4]); 377 goto end; 378 } 379 filter->limit = atoi(parv[5]); 380 } else { 381 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s :Invalid subcommand", me.name, parv[1]); 382 goto end; 383 } 384 385 if (filter->limit <= 0) 386 { 387 sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %d :Specified limit is =<0", 388 me.name, parv[1], filter->limit); 389 goto end; 390 } 391 392 if (filter->limit > CHATHISTORY_LIMIT) 393 filter->limit = CHATHISTORY_LIMIT; 394 395 if ((r = history_request(channel->name, filter))) 396 history_send_result(client, r); 397 398 end: 399 if (filter) 400 free_history_filter(filter); 401 if (r) 402 free_history_result(r); 403 }