about summary refs log tree commit diff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--bounce.h10
-rw-r--r--client.c94
-rw-r--r--pounce.18
-rw-r--r--state.c10
4 files changed, 115 insertions, 7 deletions
diff --git a/bounce.h b/bounce.h
index 8df85fc..e670ff3 100644
--- a/bounce.h
+++ b/bounce.h
@@ -25,6 +25,7 @@
  * covered work.
  */
 
+#include <err.h>
 #include <limits.h>
 #include <stdarg.h>
 #include <stdbool.h>
@@ -32,6 +33,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include <sys/time.h>
+#include <sysexits.h>
 #include <tls.h>
 
 #ifndef OPENSSL_BIN
@@ -57,6 +59,12 @@ static inline char *seprintf(char *ptr, char *end, const char *fmt, ...) {
 	return ptr + n;
 }
 
+static inline void set(char **field, const char *value) {
+	if (*field) free(*field);
+	*field = strdup(value);
+	if (!*field) err(EX_OSERR, "strdup");
+}
+
 enum { MessageCap = 8191 + 512 };
 
 enum { ParamCap = 15 };
@@ -92,6 +100,7 @@ static inline struct Message parse(char *line) {
 	X("causal.agency/consumer", CapConsumer) \
 	X("causal.agency/passive", CapPassive) \
 	X("chghost", CapChghost) \
+	X("draft/read-marker", CapReadMarker) \
 	X("echo-message", CapEchoMessage) \
 	X("extended-join", CapExtendedJoin) \
 	X("extended-monitor", CapExtendedMonitor) \
@@ -238,6 +247,7 @@ void clientSend(struct Client *client, const char *ptr, size_t len);
 void clientFormat(struct Client *client, const char *format, ...)
 	__attribute__((format(printf, 2, 3)));
 void clientConsume(struct Client *client);
+void clientGetMarker(struct Client *client, const char *target);
 
 extern bool stateNoNames;
 extern enum Cap stateCaps;
diff --git a/client.c b/client.c
index 3827b87..2227a4c 100644
--- a/client.c
+++ b/client.c
@@ -42,7 +42,13 @@
 
 #include "bounce.h"
 
-enum Cap clientCaps = CapServerTime | CapConsumer | CapPassive | CapSTS;
+enum Cap clientCaps = 0
+	| CapConsumer
+	| CapPassive
+	| CapReadMarker
+	| CapSTS
+	| CapServerTime;
+
 char *clientOrigin;
 char *clientPass;
 char *clientAway;
@@ -380,6 +386,91 @@ static void handlePalaver(struct Client *client, struct Message *msg) {
 	clientProduce(client, buf);
 }
 
+struct Marker {
+	char *target;
+	char *timestamp;
+};
+
+static struct {
+	struct Marker *ptr;
+	size_t cap, len;
+} markers;
+
+void clientGetMarker(struct Client *client, const char *target) {
+	for (size_t i = 0; i < markers.len; ++i) {
+		struct Marker marker = markers.ptr[i];
+		if (strcasecmp(marker.target, target)) continue;
+		clientFormat(
+			client, ":%s MARKREAD %s timestamp=%s\r\n",
+			clientOrigin, target, marker.timestamp
+		);
+		return;
+	}
+	clientFormat(client, ":%s MARKREAD %s *\r\n", clientOrigin, target);
+}
+
+static void clientSetMarker(
+	struct Client *client, const char *target, const char *timestamp
+) {
+	struct Marker *marker = NULL;
+	for (size_t i = 0; i < markers.len; ++i) {
+		marker = &markers.ptr[i];
+		if (strcasecmp(marker->target, target)) continue;
+		if (strcmp(timestamp, marker->timestamp) > 0) {
+			set(&marker->timestamp, timestamp);
+		}
+		goto reply;
+	}
+	if (markers.len == markers.cap) {
+		markers.cap = (markers.cap ? markers.cap * 2 : 8);
+		markers.ptr = realloc(markers.ptr, sizeof(*markers.ptr) * markers.cap);
+		if (!markers.ptr) err(EX_OSERR, "realloc");
+	}
+	marker = &markers.ptr[markers.len++];
+	*marker = (struct Marker) {0};
+	set(&marker->target, target);
+	set(&marker->timestamp, timestamp);
+reply:
+	clientFormat(
+		client, ":%s MARKREAD %s timestamp=%s\r\n",
+		clientOrigin, target, marker->timestamp
+	);
+}
+
+static regex_t *TimestampRegex(void) {
+	static const char *Pattern = {
+#define R2D "[0-9]{2}"
+		"^timestamp=[0-9]{4,}-" R2D "-" R2D
+		"T" R2D ":" R2D ":" R2D "[.][0-9]{3}Z$"
+#undef R2D
+	};
+	static bool compiled;
+	static regex_t regex;
+	if (!compiled) {
+		int error = regcomp(&regex, Pattern, REG_EXTENDED | REG_NOSUB);
+		assert(!error);
+	}
+	compiled = true;
+	return &regex;
+}
+
+static void handleMarkRead(struct Client *client, struct Message *msg) {
+	if (!msg->params[0]) {
+		clientFormat(
+			client, "FAIL MARKREAD NEED_MORE_PARAMS :Missing parameters\r\n"
+		);
+	} else if (!msg->params[1]) {
+		clientGetMarker(client, msg->params[0]);
+	} else if (regexec(TimestampRegex(), msg->params[1], 0, NULL, 0)) {
+		clientFormat(
+			client, "FAIL MARKREAD INVALID_PARAMS %s :Invalid parameters\r\n",
+			msg->params[1]
+		);
+	} else {
+		clientSetMarker(client, msg->params[0], &msg->params[1][10]);
+	}
+}
+
 static void handlePong(struct Client *client, struct Message *msg) {
 	(void)client;
 	(void)msg;
@@ -399,6 +490,7 @@ static const struct {
 	{ true, false, "CAP", handleCap },
 	{ true, false, "PALAVER", handlePalaver },
 	{ true, false, "PONG", handlePong },
+	{ true, true, "MARKREAD", handleMarkRead },
 	{ true, true, "NOTICE", handlePrivmsg },
 	{ true, true, "PRIVMSG", handlePrivmsg },
 	{ true, true, "QUIT", handleQuit },
diff --git a/pounce.1 b/pounce.1
index 79517a3..e4919d2 100644
--- a/pounce.1
+++ b/pounce.1
@@ -826,6 +826,14 @@ can be adjusted with
 .Re
 .It
 .Rs
+.%A Simon Ser
+.%A delthas
+.%T Read marker
+.%I IRCv3 Working Group
+.%U https://ircv3.net/specs/extensions/read-marker
+.Re
+.It
+.Rs
 .%A K. Zeilenga, Ed.
 .%T The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
 .%I IETF
diff --git a/state.c b/state.c
index 924bb8f..a28b3ba 100644
--- a/state.c
+++ b/state.c
@@ -53,12 +53,6 @@ static void require(const struct Message *msg, bool origin, size_t len) {
 	}
 }
 
-static void set(char **field, const char *value) {
-	if (*field) free(*field);
-	*field = strdup(value);
-	if (!*field) err(EX_OSERR, "strdup");
-}
-
 // Maximum size of one AUTHENTICATE message.
 enum { AuthLen = 299 };
 static char plainBase64[BASE64_SIZE(AuthLen)];
@@ -90,6 +84,7 @@ static const enum Cap DontReq = 0
 	| CapConsumer
 	| CapPalaverApp
 	| CapPassive
+	| CapReadMarker
 	| CapSASL
 	| CapSTS
 	| CapUnsupported;
@@ -475,6 +470,9 @@ void stateSync(struct Client *client) {
 				clientOrigin, self.nick, chan->name, chan->topic
 			);
 		}
+		if (client->caps & CapReadMarker) {
+			clientGetMarker(client, chan->name);
+		}
 		if (stateNoNames) continue;
 		serverEnqueue("NAMES %s\r\n", chan->name);
 	}