pissircd/src/modules/chathistory.c

405 lines
10 KiB
C

/* src/modules/chathistory.c - IRCv3 CHATHISTORY command.
* (C) Copyright 2021 Bram Matthys (Syzop) and the UnrealIRCd team
* License: GPLv2 or later
*
* This implements the "CHATHISTORY" command, the CAP and 005 token.
* https://ircv3.net/specs/extensions/chathistory
*/
#include "unrealircd.h"
ModuleHeader MOD_HEADER
= {
"chathistory",
"1.0",
"IRCv3 CHATHISTORY command",
"UnrealIRCd Team",
"unrealircd-6",
};
/* Structs */
typedef struct ChatHistoryTarget ChatHistoryTarget;
struct ChatHistoryTarget {
ChatHistoryTarget *prev, *next;
char *datetime;
char *object;
};
/* Forward declarations */
CMD_FUNC(cmd_chathistory);
/* Global variables */
long CAP_CHATHISTORY = 0L;
#define CHATHISTORY_LIMIT 50
MOD_INIT()
{
ClientCapabilityInfo c;
MARK_AS_OFFICIAL_MODULE(modinfo);
CommandAdd(modinfo->handle, "CHATHISTORY", cmd_chathistory, MAXPARA, CMD_USER);
memset(&c, 0, sizeof(c));
c.name = "draft/chathistory";
ClientCapabilityAdd(modinfo->handle, &c, &CAP_CHATHISTORY);
return MOD_SUCCESS;
}
MOD_LOAD()
{
ISupportSetFmt(modinfo->handle, "CHATHISTORY", "%d", CHATHISTORY_LIMIT);
ISupportSetFmt(modinfo->handle, "MSGREFTYPES", "msgid,timestamp");
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
return MOD_SUCCESS;
}
int chathistory_token(const char *str, char *token, char **store)
{
char request[BUFSIZE];
char *p;
strlcpy(request, str, sizeof(request));
p = strchr(request, '=');
if (!p)
return 0;
*p = '\0'; // frag
if (!strcmp(request, token))
{
*p = '='; // restore
*store = strdup(p + 1); // can be \0
return 1;
}
*p = '='; // restore
return 0;
}
static void add_chathistory_target_list(ChatHistoryTarget *new, ChatHistoryTarget **list)
{
ChatHistoryTarget *x, *last = NULL;
if (!*list)
{
/* We are the only item. Easy. */
*list = new;
return;
}
for (x = *list; x; x = x->next)
{
last = x;
if (strcmp(new->datetime, x->datetime) >= 0)
break;
}
if (x)
{
if (x->prev)
{
/* We will insert ourselves just before this item */
new->prev = x->prev;
new->next = x;
x->prev->next = new;
x->prev = new;
} else {
/* We are the new head */
*list = new;
new->next = x;
x->prev = new;
}
} else
{
/* We are the last item */
last->next = new;
new->prev = last;
}
}
static void add_chathistory_target(ChatHistoryTarget **list, HistoryResult *r)
{
MessageTag *m;
time_t ts;
char *datetime;
ChatHistoryTarget *e;
if (!r->log || !((m = find_mtag(r->log->mtags, "time"))) || !m->value)
return;
datetime = m->value;
e = safe_alloc(sizeof(ChatHistoryTarget));
safe_strdup(e->datetime, datetime);
safe_strdup(e->object, r->object);
add_chathistory_target_list(e, list);
}
static void chathistory_targets_send_line(Client *client, ChatHistoryTarget *r, char *batchid)
{
MessageTag *mtags = NULL;
MessageTag *m;
if (!BadPtr(batchid))
{
mtags = safe_alloc(sizeof(MessageTag));
mtags->name = strdup("batch");
mtags->value = strdup(batchid);
}
sendto_one(client, mtags, ":%s CHATHISTORY TARGETS %s %s",
me.name, r->object, r->datetime);
if (mtags)
free_message_tags(mtags);
}
void chathistory_targets(Client *client, HistoryFilter *filter, int limit)
{
Membership *mp;
HistoryResult *r;
char batch[BATCHLEN+1];
int sent = 0;
ChatHistoryTarget *targets = NULL, *targets_next;
/* 1. Grab all information we need */
filter->cmd = HFC_BEFORE;
if (strcmp(filter->timestamp_a, filter->timestamp_b) < 0)
{
/* Swap if needed */
char *swap = filter->timestamp_a;
filter->timestamp_a = filter->timestamp_b;
filter->timestamp_b = swap;
}
filter->limit = 1;
for (mp = client->user->channel; mp; mp = mp->next)
{
Channel *channel = mp->channel;
r = history_request(channel->name, filter);
if (r)
{
add_chathistory_target(&targets, r);
free_history_result(r);
}
}
/* 2. Now send it to the client */
batch[0] = '\0';
if (HasCapability(client, "batch"))
{
/* Start a new batch */
generate_batch_id(batch);
sendto_one(client, NULL, ":%s BATCH +%s draft/chathistory-targets", me.name, batch);
}
for (; targets; targets = targets_next)
{
targets_next = targets->next;
if (++sent < limit)
chathistory_targets_send_line(client, targets, batch);
safe_free(targets->datetime);
safe_free(targets->object);
safe_free(targets);
}
/* End of batch */
if (*batch)
sendto_one(client, NULL, ":%s BATCH -%s", me.name, batch);
}
void send_empty_batch(Client *client, const char *target)
{
char batch[BATCHLEN+1];
if (HasCapability(client, "batch"))
{
generate_batch_id(batch);
sendto_one(client, NULL, ":%s BATCH +%s chathistory %s", me.name, batch, target);
sendto_one(client, NULL, ":%s BATCH -%s", me.name, batch);
}
}
CMD_FUNC(cmd_chathistory)
{
HistoryFilter *filter = NULL;
HistoryResult *r = NULL;
Channel *channel;
memset(&filter, 0, sizeof(filter));
/* This command is only for local users */
if (!MyUser(client))
return;
if ((parc < 5) || BadPtr(parv[4]))
{
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS :Insufficient parameters", me.name);
return;
}
if (!HasCapability(client, "server-time"))
{
sendnotice(client, "Your IRC client does not support the 'server-time' capability");
sendnotice(client, "https://ircv3.net/specs/extensions/server-time");
sendnotice(client, "History request refused.");
return;
}
if (!strcasecmp(parv[1], "TARGETS"))
{
Membership *mp;
int limit;
filter = safe_alloc(sizeof(HistoryFilter));
/* Below this point, instead of 'return', use 'goto end' */
if (!chathistory_token(parv[2], "timestamp", &filter->timestamp_a))
{
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx",
me.name, parv[1], parv[3]);
goto end;
}
if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_b))
{
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx",
me.name, parv[1], parv[4]);
goto end;
}
limit = atoi(parv[4]);
chathistory_targets(client, filter, limit);
goto end;
}
/* We don't support retrieving chathistory for PM's. Send empty response/batch, similar to channels without +H. */
if (parv[2][0] != '#')
{
send_empty_batch(client, parv[2]);
return;
}
channel = find_channel(parv[2]);
if (!channel)
{
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_TARGET %s %s :Messages could not be retrieved, not an existing channel",
me.name, parv[1], parv[2]);
return;
}
if (!IsMember(client, channel))
{
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_TARGET %s %s :Messages could not be retrieved, you are not a member",
me.name, parv[1], parv[2]);
return;
}
/* Channel is not +H? Send empty response/batch (as per IRCv3 discussion) */
if (!has_channel_mode(channel, 'H'))
{
send_empty_batch(client, channel->name);
return;
}
filter = safe_alloc(sizeof(HistoryFilter));
/* Below this point, instead of 'return', use 'goto end', which takes care of the freeing of 'filter' and 'history' */
if (!strcasecmp(parv[1], "BEFORE"))
{
filter->cmd = HFC_BEFORE;
if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
!chathistory_token(parv[3], "msgid", &filter->msgid_a))
{
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
me.name, parv[1], parv[3]);
goto end;
}
filter->limit = atoi(parv[4]);
} else
if (!strcasecmp(parv[1], "AFTER"))
{
filter->cmd = HFC_AFTER;
if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
!chathistory_token(parv[3], "msgid", &filter->msgid_a))
{
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
me.name, parv[1], parv[3]);
goto end;
}
filter->limit = atoi(parv[4]);
} else
if (!strcasecmp(parv[1], "LATEST"))
{
filter->cmd = HFC_LATEST;
if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
!chathistory_token(parv[3], "msgid", &filter->msgid_a) &&
strcmp(parv[3], "*"))
{
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx or *",
me.name, parv[1], parv[3]);
goto end;
}
filter->limit = atoi(parv[4]);
} else
if (!strcasecmp(parv[1], "AROUND"))
{
filter->cmd = HFC_AROUND;
if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
!chathistory_token(parv[3], "msgid", &filter->msgid_a))
{
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
me.name, parv[1], parv[3]);
goto end;
}
filter->limit = atoi(parv[4]);
} else
if (!strcasecmp(parv[1], "BETWEEN"))
{
filter->cmd = HFC_BETWEEN;
if (BadPtr(parv[5]))
{
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s :Insufficient parameters", parv[1], me.name);
goto end;
}
if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
!chathistory_token(parv[3], "msgid", &filter->msgid_a))
{
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
me.name, parv[1], parv[3]);
goto end;
}
if (!chathistory_token(parv[4], "timestamp", &filter->timestamp_b) &&
!chathistory_token(parv[4], "msgid", &filter->msgid_b))
{
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
me.name, parv[1], parv[4]);
goto end;
}
filter->limit = atoi(parv[5]);
} else {
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s :Invalid subcommand", me.name, parv[1]);
goto end;
}
if (filter->limit <= 0)
{
sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %d :Specified limit is =<0",
me.name, parv[1], filter->limit);
goto end;
}
if (filter->limit > CHATHISTORY_LIMIT)
filter->limit = CHATHISTORY_LIMIT;
if ((r = history_request(channel->name, filter)))
history_send_result(client, r);
end:
if (filter)
free_history_filter(filter);
if (r)
free_history_result(r);
}