From 3b1c1b835c6439507b68d7666df61fa96e61095c Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sun, 24 Jan 2021 00:49:53 -0500 Subject: Support echo-message capability Only request it with labeled-response, since it is impossible to correlate messages to clients without. For clients without echo-message, synthesize a label on PRIVMSG/NOTICE/TAGMSG, then filter out received messages with that label. --- bounce.h | 1 + client.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++----------------- pounce.1 | 3 ++- state.c | 3 +++ 4 files changed, 73 insertions(+), 25 deletions(-) diff --git a/bounce.h b/bounce.h index 2bcd77c..a2265c8 100644 --- a/bounce.h +++ b/bounce.h @@ -83,6 +83,7 @@ static inline struct Message parse(char *line) { X("causal.agency/consumer", CapConsumer) \ X("causal.agency/passive", CapPassive) \ X("chghost", CapChghost) \ + X("echo-message", CapEchoMessage) \ X("extended-join", CapExtendedJoin) \ X("invite-notify", CapInviteNotify) \ X("labeled-response", CapLabeledResponse) \ diff --git a/client.c b/client.c index 7b0abba..c4cfe14 100644 --- a/client.c +++ b/client.c @@ -259,6 +259,27 @@ static void handleQuit(struct Client *client, struct Message *msg) { client->error = true; } +static bool hasTag(const struct Message *msg, const char *key) { + if (!msg->tags) return false; + size_t len = strlen(key); + for (const char *tags = msg->tags; *tags;) { + if ( + !strncmp(tags, key, len) && + (!tags[len] || tags[len] == ';' || tags[len] == '=') + ) return true; + tags += strcspn(tags, ";"); + tags += (*tags == ';'); + } + return false; +} + +static const char *synthLabel(struct Client *client) { + enum { LabelCap = 64 }; + static char buf[sizeof("label=") + LabelCap]; + snprintf(buf, sizeof(buf), "label=pounce~%zu", client->consumer); + return buf; +} + static void reserialize( char *buf, size_t cap, const char *origin, const struct Message *msg ) { @@ -287,27 +308,31 @@ static void reserialize( static void clientProduce(struct Client *client, const char *line) { size_t diff = ringDiff(client->consumer); ringProduce(line); - if (!diff) ringConsume(NULL, client->consumer); + if (!diff && !(client->caps & CapEchoMessage)) { + ringConsume(NULL, client->consumer); + } } static void handlePrivmsg(struct Client *client, struct Message *msg) { if (!msg->params[0]) return; char buf[MessageCap]; - reserialize(buf, sizeof(buf), stateEcho(), msg); - clientProduce(client, buf); - if (!strcmp(msg->params[0], stateNick())) return; - reserialize(buf, sizeof(buf), NULL, msg); - serverFormat("%s\r\n", buf); -} - -static void handleTagmsg(struct Client *client, struct Message *msg) { - if (!msg->params[0]) return; - char buf[MessageCap]; - reserialize(buf, sizeof(buf), stateEcho(), msg); - clientProduce(client, buf); - if (!strcmp(msg->params[0], stateNick())) return; + bool self = !strcmp(msg->params[0], stateNick()); + if (!(stateCaps & CapEchoMessage) || self) { + reserialize(buf, sizeof(buf), stateEcho(), msg); + clientProduce(client, buf); + } + if (self) return; reserialize(buf, sizeof(buf), NULL, msg); - serverFormat("%s\r\n", buf); + if (stateCaps & CapEchoMessage && !hasTag(msg, "label")) { + serverFormat( + "@%s%c%s\r\n", + synthLabel(client), + (buf[0] == '@' ? ';' : ' '), + (buf[0] == '@' ? &buf[1] : buf) + ); + } else { + serverFormat("%s\r\n", buf); + } } static void handlePalaver(struct Client *client, struct Message *msg) { @@ -332,7 +357,7 @@ static const struct { { true, true, "NOTICE", handlePrivmsg }, { true, true, "PRIVMSG", handlePrivmsg }, { true, true, "QUIT", handleQuit }, - { true, true, "TAGMSG", handleTagmsg }, + { true, true, "TAGMSG", handlePrivmsg }, }; static void clientParse(struct Client *client, char *line) { @@ -551,12 +576,6 @@ static const char *filterUserhostInNames(const char *line) { ); } -static const char *filterTags(const char *line) { - if (line[0] != '@') return line; - const char *sp = strchr(line, ' '); - return (sp ? sp + 1 : NULL); -} - static Filter *Filters[] = { [CapAccountNotifyBit] = filterAccountNotify, [CapAwayNotifyBit] = filterAwayNotify, @@ -573,6 +592,27 @@ static Filter *Filters[] = { [CapUserhostInNamesBit] = filterUserhostInNames, }; +static const char *filterEchoMessage(struct Client *client, const char *line) { + if (line[0] != '@') return line; + const char *label = synthLabel(client); + size_t len = strlen(label); + for (const char *tags = &line[1]; *tags != ' ';) { + if ( + !strncmp(tags, label, len) && + (tags[len] == ' ' || tags[len] == ';') + ) return NULL; + tags += strcspn(tags, "; "); + tags += (*tags == ';'); + } + return line; +} + +static const char *filterTags(const char *line) { + if (line[0] != '@') return line; + const char *sp = strchr(line, ' '); + return (sp ? sp + 1 : NULL); +} + static bool hasTime(const char *line) { if (!strncmp(line, "@time=", 6)) return true; while (*line && *line != ' ') { @@ -588,10 +628,13 @@ void clientConsume(struct Client *client) { const char *line = ringPeek(&time, client->consumer); if (!line) return; - if (stateCaps & TagCaps && !(client->caps & TagCaps)) { + enum Cap diff = client->caps ^ (clientCaps | stateCaps); + if (diff & CapEchoMessage) { + line = filterEchoMessage(client, line); + } + if (line && stateCaps & TagCaps && !(client->caps & TagCaps)) { line = filterTags(line); } - enum Cap diff = client->caps ^ (clientCaps | stateCaps); for (size_t i = 0; line && i < ARRAY_LEN(Filters); ++i) { if (!Filters[i]) continue; if (diff & (1 << i)) line = Filters[i](line); diff --git a/pounce.1 b/pounce.1 index caeb243..148180f 100644 --- a/pounce.1 +++ b/pounce.1 @@ -1,4 +1,4 @@ -.Dd January 11, 2021 +.Dd January 23, 2021 .Dt POUNCE 1 .Os . @@ -451,6 +451,7 @@ is supported: .Sy batch , .Sy cap-notify , .Sy chghost , +.Sy echo-message , .Sy extended-join , .Sy invite-notify , .Sy labeled-response , diff --git a/state.c b/state.c index 41ffdd0..6b26589 100644 --- a/state.c +++ b/state.c @@ -90,6 +90,9 @@ static void handleCap(struct Message *msg) { if (!strcmp(msg->params[1], "LS") || !strcmp(msg->params[1], "NEW")) { caps &= ~(CapSASL | CapSTS | CapUnsupported); + if (caps & CapEchoMessage && !(caps & CapLabeledResponse)) { + caps &= ~CapEchoMessage; + } if (caps) { serverFormat("CAP REQ :%s\r\n", capList(caps, NULL)); } else { -- cgit 1.4.1