summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--unscoop.116
-rw-r--r--unscoop.c74
2 files changed, 78 insertions, 12 deletions
diff --git a/unscoop.1 b/unscoop.1
index 4dbd01e..2a87fc9 100644
--- a/unscoop.1
+++ b/unscoop.1
@@ -1,4 +1,4 @@
-.Dd September 30, 2020
+.Dd May 17, 2021
 .Dt UNSCOOP 1
 .Os
 .
@@ -104,6 +104,20 @@ find Textual -type f -name '*.txt' \e
 xargs -0 unscoop -f textual
 .Ed
 .
+.It Fl f Cm znc
+Import logs from the
+.Xr znc 1
+.Sy log
+module.
+.Bd -literal -offset indent
+find ~/.znc/moddata/log \e
+	~/.znc/users/*/moddata/log \e
+	~/.znc/users/*/networks/*/moddata/log \e
+	-type f -name '*.log' \e
+	-not -path '*/status/*' -print0 |
+xargs -0 unscoop -f znc
+.Ed
+.
 .It Fl v
 Print SQL
 .Sy INSERT
diff --git a/unscoop.c b/unscoop.c
index e7e706d..598979b 100644
--- a/unscoop.c
+++ b/unscoop.c
@@ -49,6 +49,7 @@ struct Matcher {
 
 #define P0_MODE "[!~&@%+ ]?"
 #define P1_TIME "^[[]([^]]+)[]][ \t]"
+#define P2_USERHOST "[(]([^@]+)@([^)]+)[)]"
 
 static const struct Matcher Catgirl[] = {
 	{
@@ -147,7 +148,6 @@ static const struct Matcher IRC[] = {
 #undef P2_TAGS
 #undef P3_ORIGIN
 
-#define P2_USERHOST "[(]([^@]+)@([^)]+)[)]"
 #define P2_MESSAGE "( [(]([^)]+)[)])?"
 static const struct Matcher Textual[] = {
 	{
@@ -185,9 +185,45 @@ static const struct Matcher Textual[] = {
 		Unban, { ":time", ":nick", ":target" },
 	}
 };
-#undef P2_USERHOST
 #undef P2_MESSAGE
 
+static const struct Matcher ZNC[] = {
+	{
+		P1_TIME "<([^>]+)> (.+)",
+		Privmsg, { ":time", ":nick", ":message" },
+	}, {
+		P1_TIME "-([^-]+)- (.+)",
+		Notice, { ":time", ":nick", ":message" },
+	}, {
+		P1_TIME "[*] ([^ ]+) (.+)",
+		Action, { ":time", ":nick", ":message" },
+	}, {
+		P1_TIME "[*]{3} Joins: ([^ ]+) " P2_USERHOST,
+		Join, { ":time", ":nick", ":user", ":host" },
+	}, {
+		P1_TIME "[*]{3} Parts: ([^ ]+) " P2_USERHOST " [(](.*)[)]",
+		Part, { ":time", ":nick", ":user", ":host", ":message" },
+	}, {
+		P1_TIME "[*]{3} ([^ ]+) was kicked by ([^ ]+) [(](.*)[)]",
+		Kick, { ":time", ":target", ":nick", ":message" },
+	}, {
+		P1_TIME "[*]{3} Quits: ([^ ]+) " P2_USERHOST " [(](.*)[)]",
+		Quit, { ":time", ":nick", ":user", ":host", ":message" },
+	}, {
+		P1_TIME "[*]{3} ([^ ]+) is now known as ([^ ]+)",
+		Nick, { ":time", ":nick", ":target" },
+	}, {
+		P1_TIME "[*]{3} ([^ ]+) changes topic to '(.*)'",
+		Topic, { ":time", ":nick", ":message" },
+	}, {
+		P1_TIME "[*]{3} ([^ ]+) sets mode: [+]b+ (.+)",
+		Ban, { ":time", ":nick", ":target" },
+	}, {
+		P1_TIME "[*]{3} ([^ ]+) sets mode: [-]b+ (.+)",
+		Unban, { ":time", ":nick", ":target" },
+	}
+};
+
 static const struct Format {
 	const char *name;
 	const struct Matcher *matchers;
@@ -195,18 +231,19 @@ static const struct Format {
 	const char *pattern;
 	size_t network;
 	size_t context;
+	size_t date;
 } Formats[] = {
 	{
 		"generic", Generic, ARRAY_LEN(Generic),
-		"([^/]+)/([^/]+)/[^/]+$", 1, 2,
+		"([^/]+)/([^/]+)/[^/]+$", 1, 2, 0,
 	},
 	{
 		"catgirl", Catgirl, ARRAY_LEN(Catgirl),
-		"([^/]+)/([^/]+)/[0-9-]+[.]log$", 1, 2,
+		"([^/]+)/([^/]+)/[0-9-]+[.]log$", 1, 2, 0,
 	},
 	{
 		"irc", IRC, ARRAY_LEN(IRC),
-		"^$", 0, 0,
+		"^$", 0, 0, 0,
 	},
 	{
 		"textual", Textual, ARRAY_LEN(Textual),
@@ -216,7 +253,11 @@ static const struct Format {
 			"([^/]+)/"
 			"[0-9-]+[.]txt$"
 		),
-		1, 4,
+		1, 4, 0,
+	},
+	{
+		"znc", ZNC, ARRAY_LEN(ZNC),
+		"([^/]+)/(moddata/log/)?([^/]+)/([0-9-]+)[.]log$", 1, 3, 4,
 	},
 };
 
@@ -253,6 +294,7 @@ static sqlite3_stmt *insertName;
 static sqlite3_stmt *insertEvent;
 static int paramNetwork;
 static int paramContext;
+static int paramDate;
 
 static void prepareInsert(void) {
 	const char *InsertName = SQL(
@@ -265,9 +307,11 @@ static void prepareInsert(void) {
 		INSERT INTO events (time, type, context, name, target, message)
 		SELECT
 			// SQLite expects a colon in the timezine, but ISO8601 does not.
-			CASE WHEN :time LIKE '%Z'
-				THEN strftime('%s', :time)
-				ELSE strftime('%s', substr(:time, 1, 22) || ':' || substr(:time, -2))
+			CASE
+			WHEN :time LIKE '%+____' OR :time LIKE '%-____' THEN
+				strftime('%s', substr(:time, 1, 22) || ':' || substr(:time, -2))
+			ELSE
+				strftime('%s', coalesce(:date || ' ', "") || :time)
 			END,
 			:type, context, names.name, :target, :message
 		FROM contexts, names
@@ -280,6 +324,7 @@ static void prepareInsert(void) {
 	dbPersist(&insertEvent, InsertEvent);
 	paramNetwork = dbParam(insertEvent, ":network");
 	paramContext = dbParam(insertEvent, ":context");
+	paramDate = dbParam(insertEvent, ":date");
 }
 
 static void
@@ -291,8 +336,9 @@ matchLine(const struct Format *format, const regex_t *regex, const char *line) {
 
 		sqlite3_clear_bindings(insertName);
 		for (int i = 1; i <= sqlite3_bind_parameter_count(insertEvent); ++i) {
-			if (i == paramNetwork || i == paramContext) continue;
-			sqlite3_bind_null(insertEvent, i);
+			if (i != paramNetwork && i != paramContext && i != paramDate) {
+				sqlite3_bind_null(insertEvent, i);
+			}
 		}
 
 		dbBindInt(insertEvent, ":type", matcher->type);
@@ -430,6 +476,12 @@ int main(int argc, char *argv[]) {
 		}
 		dbRun(insertContext);
 
+		if (format->date) {
+			bindMatch(
+				insertEvent, ":date", argv[i], paths[i].match[format->date]
+			);
+		}
+
 		for (ssize_t len; 0 < (len = getline(&line, &cap, file));) {
 			matchLine(format, regex, line);
 			sizeRead += len;