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 }