about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Makefile1
-rw-r--r--README.74
-rw-r--r--catgirl.126
-rw-r--r--chat.c4
-rw-r--r--chat.h11
-rw-r--r--command.c31
-rw-r--r--handle.c16
-rw-r--r--ignore.c73
8 files changed, 154 insertions, 12 deletions
diff --git a/Makefile b/Makefile
index ec838f5..4a610b7 100644
--- a/Makefile
+++ b/Makefile
@@ -12,6 +12,7 @@ OBJS += complete.o
 OBJS += config.o
 OBJS += edit.o
 OBJS += handle.o
+OBJS += ignore.o
 OBJS += irc.o
 OBJS += log.o
 OBJS += ui.o
diff --git a/README.7 b/README.7
index 0362661..bfc00d4 100644
--- a/README.7
+++ b/README.7
@@ -1,4 +1,4 @@
-.Dd March 25, 2020
+.Dd March 31, 2020
 .Dt README 7
 .Os "Causal Agency"
 .
@@ -132,6 +132,8 @@ line editing
 tab complete
 .It Pa url.c
 URL detection
+.It Pa ignore.c
+message filtering
 .It Pa log.c
 chat logging
 .It Pa config.c
diff --git a/catgirl.1 b/catgirl.1
index 42569a3..4571162 100644
--- a/catgirl.1
+++ b/catgirl.1
@@ -1,4 +1,4 @@
-.Dd March 30, 2020
+.Dd March 31, 2020
 .Dt CATGIRL 1
 .Os
 .
@@ -17,6 +17,7 @@
 .Op Fl a Ar auth
 .Op Fl c Ar cert
 .Op Fl h Ar host
+.Op Fl i Ar patt
 .Op Fl j Ar join
 .Op Fl k Ar priv
 .Op Fl n Ar nick
@@ -147,6 +148,22 @@ and write it to
 Connect to
 .Ar host .
 .
+.It Fl i Ar pattern, Cm ignore = Ar pattern
+Add a case-insensitive message filtering pattern,
+which may contain
+.Ql * ,
+.Ql \&?
+and
+.Ql []
+wildcards as in
+.Xr sh 1 .
+The format of the pattern is as follows:
+.Bd -ragged -offset indent
+.Ar nick Ns Op Ar !user@host
+.Op Ar command
+.Op Ar channel
+.Ed
+.
 .It Fl j Ar join , Cm join = Ar join
 Join the comma-separated list of channels
 .Ar join .
@@ -309,6 +326,11 @@ Type
 .Ic q
 to return to
 .Nm .
+.It Ic /ignore Op Ar pattern
+List message filtering patterns
+or temporarily add a pattern.
+To permanently add a pattern, use
+.Fl i .
 .It Ic /move Oo Ar name Oc Ar num
 Move named window to number.
 .It Ic /open Op Ar count
@@ -320,6 +342,8 @@ Open the most recent URL from
 .Ar nick
 or matching
 .Ar substring .
+.It Ic /unignore Ar pattern
+Temporarily remove a message filtering pattern.
 .It Ic /window Ar name
 Switch to window by name.
 .It Ic /window Ar num , Ic / Ns Ar num
diff --git a/chat.c b/chat.c
index b7bf91b..8ead5da 100644
--- a/chat.c
+++ b/chat.c
@@ -127,7 +127,7 @@ int main(int argc, char *argv[]) {
 	const char *user = NULL;
 	const char *real = NULL;
 
-	const char *Opts = "!C:H:N:O:RS:a:c:eg:h:j:k:ln:p:r:s:u:vw:";
+	const char *Opts = "!C:H:N:O:RS:a:c:eg:h:i:j:k:ln:p:r:s:u:vw:";
 	const struct option LongOpts[] = {
 		{ "insecure", no_argument, NULL, '!' },
 		{ "copy", required_argument, NULL, 'C' },
@@ -140,6 +140,7 @@ int main(int argc, char *argv[]) {
 		{ "cert", required_argument, NULL, 'c' },
 		{ "sasl-external", no_argument, NULL, 'e' },
 		{ "host", required_argument, NULL, 'h' },
+		{ "ignore", required_argument, NULL, 'i' },
 		{ "join", required_argument, NULL, 'j' },
 		{ "priv", required_argument, NULL, 'k' },
 		{ "log", no_argument, NULL, 'l' },
@@ -168,6 +169,7 @@ int main(int argc, char *argv[]) {
 			break; case 'e': sasl = true;
 			break; case 'g': genCert(optarg);
 			break; case 'h': host = optarg;
+			break; case 'i': ignoreAdd(optarg);
 			break; case 'j': self.join = optarg;
 			break; case 'k': priv = optarg;
 			break; case 'l': logEnable = true;
diff --git a/chat.h b/chat.h
index 545be03..7bcac3b 100644
--- a/chat.h
+++ b/chat.h
@@ -200,7 +200,7 @@ const char *commandIsNotice(uint id, const char *input);
 const char *commandIsAction(uint id, const char *input);
 void commandCompleteAdd(void);
 
-enum Heat { Cold, Warm, Hot };
+enum Heat { Ice, Cold, Warm, Hot };
 extern struct Util uiNotifyUtil;
 void uiInit(void);
 void uiShow(void);
@@ -261,6 +261,15 @@ void urlOpenCount(uint id, uint count);
 void urlOpenMatch(uint id, const char *str);
 void urlCopyMatch(uint id, const char *str);
 
+enum { IgnoreCap = 256 };
+extern struct Ignore {
+	size_t len;
+	char *patterns[IgnoreCap];
+} ignore;
+const char *ignoreAdd(const char *pattern);
+bool ignoreRemove(const char *pattern);
+enum Heat ignoreCheck(enum Heat heat, const struct Message *msg);
+
 extern bool logEnable;
 void logFormat(uint id, const time_t *time, const char *format, ...)
 	__attribute__((format(printf, 3, 4)));
diff --git a/command.c b/command.c
index 8782ee6..5872bdc 100644
--- a/command.c
+++ b/command.c
@@ -348,6 +348,35 @@ static void commandCopy(uint id, char *params) {
 	urlCopyMatch(id, params);
 }
 
+static void commandIgnore(uint id, char *params) {
+	if (params) {
+		const char *pattern = ignoreAdd(params);
+		uiFormat(
+			id, Cold, NULL, "Ignoring \3%02d%s\3",
+			Brown, pattern
+		);
+	} else {
+		for (size_t i = 0; i < ignore.len; ++i) {
+			uiFormat(
+				Network, Warm, NULL, "Ignoring \3%02d%s\3",
+				Brown, ignore.patterns[i]
+			);
+		}
+	}
+}
+
+static void commandUnignore(uint id, char *params) {
+	if (!params) return;
+	if (ignoreRemove(params)) {
+		uiFormat(
+			id, Cold, NULL, "No longer ignoring \3%02d%s\3",
+			Brown, params
+		);
+	} else {
+		uiFormat(id, Cold, NULL, "Not ignoring \3%02d%s\3", Brown, params);
+	}
+}
+
 static void commandExec(uint id, char *params) {
 	execID = id;
 
@@ -404,6 +433,7 @@ static const struct Handler {
 	{ "/except", commandExcept, 0 },
 	{ "/exec", commandExec, Multiline | Restricted },
 	{ "/help", commandHelp, 0 },
+	{ "/ignore", commandIgnore, 0 },
 	{ "/invex", commandInvex, 0 },
 	{ "/invite", commandInvite, 0 },
 	{ "/join", commandJoin, Restricted },
@@ -428,6 +458,7 @@ static const struct Handler {
 	{ "/topic", commandTopic, 0 },
 	{ "/unban", commandUnban, 0 },
 	{ "/unexcept", commandUnexcept, 0 },
+	{ "/unignore", commandUnignore, 0 },
 	{ "/uninvex", commandUninvex, 0 },
 	{ "/voice", commandVoice, 0 },
 	{ "/whois", commandWhois, 0 },
diff --git a/handle.c b/handle.c
index 84a2927..a98c316 100644
--- a/handle.c
+++ b/handle.c
@@ -305,7 +305,7 @@ static void handleJoin(struct Message *msg) {
 		msg->params[2] = NULL;
 	}
 	uiFormat(
-		id, Cold, tagTime(msg),
+		id, ignoreCheck(Cold, msg), tagTime(msg),
 		"\3%02d%s\3\t%s%s%sarrives in \3%02d%s\3",
 		hash(msg->user), msg->nick,
 		(msg->params[2] ? "(" : ""),
@@ -337,7 +337,7 @@ static void handlePart(struct Message *msg) {
 	completeRemove(id, msg->nick);
 	urlScan(id, msg->nick, msg->params[1]);
 	uiFormat(
-		id, Cold, tagTime(msg),
+		id, ignoreCheck(Cold, msg), tagTime(msg),
 		"\3%02d%s\3\tleaves \3%02d%s\3%s%s",
 		hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0],
 		(msg->params[1] ? ": " : ""),
@@ -388,7 +388,7 @@ static void handleNick(struct Message *msg) {
 			set(&idNames[id], msg->params[0]);
 		}
 		uiFormat(
-			id, Cold, tagTime(msg),
+			id, ignoreCheck(Cold, msg), tagTime(msg),
 			"\3%02d%s\3\tis now known as \3%02d%s\3",
 			hash(msg->user), msg->nick, hash(msg->user), msg->params[0]
 		);
@@ -406,7 +406,7 @@ static void handleQuit(struct Message *msg) {
 	for (uint id; (id = completeID(msg->nick));) {
 		urlScan(id, msg->nick, msg->params[0]);
 		uiFormat(
-			id, Cold, tagTime(msg),
+			id, ignoreCheck(Cold, msg), tagTime(msg),
 			"\3%02d%s\3\tleaves%s%s",
 			hash(msg->user), msg->nick,
 			(msg->params[0] ? ": " : ""),
@@ -427,7 +427,7 @@ static void handleInvite(struct Message *msg) {
 	require(msg, true, 2);
 	if (!strcmp(msg->params[0], self.nick)) {
 		uiFormat(
-			Network, Hot, tagTime(msg),
+			Network, ignoreCheck(Hot, msg), tagTime(msg),
 			"\3%02d%s\3\tinvites you to \3%02d%s\3",
 			hash(msg->user), msg->nick, hash(msg->params[1]), msg->params[1]
 		);
@@ -1103,7 +1103,7 @@ static void handlePrivmsg(struct Message *msg) {
 			logFormat(id, tagTime(msg), "-%s- %s", msg->nick, msg->params[1]);
 		}
 		uiFormat(
-			id, Warm, tagTime(msg),
+			id, ignoreCheck(Warm, msg), tagTime(msg),
 			"\3%d-%s-\3%d\t%s",
 			hash(msg->user), msg->nick, LightGray, msg->params[1]
 		);
@@ -1111,7 +1111,7 @@ static void handlePrivmsg(struct Message *msg) {
 		logFormat(id, tagTime(msg), "* %s %s", msg->nick, msg->params[1]);
 		const char *mentions = colorMentions(id, msg);
 		uiFormat(
-			id, (mention || query ? Hot : Warm), tagTime(msg),
+			id, ignoreCheck((mention || query ? Hot : Warm), msg), tagTime(msg),
 			"%s\35\3%d* %s\17\35\t%s%s",
 			(mention ? "\26" : ""), hash(msg->user), msg->nick,
 			mentions, msg->params[1]
@@ -1120,7 +1120,7 @@ static void handlePrivmsg(struct Message *msg) {
 		logFormat(id, tagTime(msg), "<%s> %s", msg->nick, msg->params[1]);
 		const char *mentions = colorMentions(id, msg);
 		uiFormat(
-			id, (mention || query ? Hot : Warm), tagTime(msg),
+			id, ignoreCheck((mention || query ? Hot : Warm), msg), tagTime(msg),
 			"%s\3%d<%s>\17\t%s%s",
 			(mention ? "\26" : ""), hash(msg->user), msg->nick,
 			mentions, msg->params[1]
diff --git a/ignore.c b/ignore.c
new file mode 100644
index 0000000..5c14b7d
--- /dev/null
+++ b/ignore.c
@@ -0,0 +1,73 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <fnmatch.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+
+#include "chat.h"
+
+struct Ignore ignore;
+
+const char *ignoreAdd(const char *pattern) {
+	if (ignore.len == IgnoreCap) errx(EX_CONFIG, "ignore limit exceeded");
+	uint ex = 0, sp = 0;
+	for (const char *ch = pattern; *ch; ++ch) {
+		if (*ch == '!') ex++;
+		if (*ch == ' ') sp++;
+	}
+	char **dest = &ignore.patterns[ignore.len++];
+	if (!ex && !sp) {
+		asprintf(dest, "%s!*@* * *", pattern);
+	} else if (sp < 1) {
+		asprintf(dest, "%s * *", pattern);
+	} else if (sp < 2) {
+		asprintf(dest, "%s *", pattern);
+	} else {
+		*dest = strdup(pattern);
+	}
+	if (!*dest) err(EX_OSERR, "strdup");
+	return *dest;
+}
+
+bool ignoreRemove(const char *pattern) {
+	bool found = false;
+	for (size_t i = 0; i < ignore.len; ++i) {
+		if (strcasecmp(ignore.patterns[i], pattern)) continue;
+		free(ignore.patterns[i]);
+		ignore.patterns[i] = ignore.patterns[--ignore.len];
+		found = true;
+	}
+	return found;
+}
+
+enum Heat ignoreCheck(enum Heat heat, const struct Message *msg) {
+	char match[512];
+	snprintf(
+		match, sizeof(match), "%s!%s@%s %s %s",
+		msg->nick, msg->user, msg->host, msg->cmd,
+		(msg->params[0] ? msg->params[0] : "")
+	);
+	for (size_t i = 0; i < ignore.len; ++i) {
+		if (fnmatch(ignore.patterns[i], match, FNM_CASEFOLD)) continue;
+		return Ice;
+	}
+	return heat;
+}