summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Makefile1
-rw-r--r--README.74
-rw-r--r--catgirl.18
-rw-r--r--chat.c5
-rw-r--r--chat.h6
-rw-r--r--log.c112
-rw-r--r--xdg.c22
7 files changed, 154 insertions, 4 deletions
diff --git a/Makefile b/Makefile
index ceaaf38..ec838f5 100644
--- a/Makefile
+++ b/Makefile
@@ -13,6 +13,7 @@ OBJS += config.o
 OBJS += edit.o
 OBJS += handle.o
 OBJS += irc.o
+OBJS += log.o
 OBJS += ui.o
 OBJS += url.o
 OBJS += xdg.o
diff --git a/README.7 b/README.7
index 9daf378..0362661 100644
--- a/README.7
+++ b/README.7
@@ -1,4 +1,4 @@
-.Dd February 12, 2020
+.Dd March 25, 2020
 .Dt README 7
 .Os "Causal Agency"
 .
@@ -132,6 +132,8 @@ line editing
 tab complete
 .It Pa url.c
 URL detection
+.It Pa log.c
+chat logging
 .It Pa config.c
 configuration parsing
 .It Pa xdg.c
diff --git a/catgirl.1 b/catgirl.1
index c3547be..501163c 100644
--- a/catgirl.1
+++ b/catgirl.1
@@ -1,4 +1,4 @@
-.Dd March 23, 2020
+.Dd March 25, 2020
 .Dt CATGIRL 1
 .Os
 .
@@ -8,7 +8,7 @@
 .
 .Sh SYNOPSIS
 .Nm
-.Op Fl Rev
+.Op Fl Relv
 .Op Fl C Ar copy
 .Op Fl H Ar hash
 .Op Fl N Ar send
@@ -155,6 +155,10 @@ Join the comma-separated list of channels
 Load the TLS client private key from
 .Ar path .
 .
+.It Fl l , Cm log
+Log chat events to files in paths
+.Pa $XDG_DATA_HOME/catgirl/log/network/channel/YYYY-MM-DD.log .
+.
 .It Fl n Ar nick , Cm nick = Ar nick
 Set nickname to
 .Ar nick .
diff --git a/chat.c b/chat.c
index 9e3e374..35c0ecd 100644
--- a/chat.c
+++ b/chat.c
@@ -129,7 +129,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:n:p:r:s:u:vw:";
+	const char *Opts = "!C:H:N:O:RS:a:c:eg:h:j:k:ln:p:r:s:u:vw:";
 	const struct option LongOpts[] = {
 		{ "insecure", no_argument, NULL, '!' },
 		{ "copy", required_argument, NULL, 'C' },
@@ -144,6 +144,7 @@ int main(int argc, char *argv[]) {
 		{ "host", required_argument, NULL, 'h' },
 		{ "join", required_argument, NULL, 'j' },
 		{ "priv", required_argument, NULL, 'k' },
+		{ "log", no_argument, NULL, 'l' },
 		{ "nick", required_argument, NULL, 'n' },
 		{ "port", required_argument, NULL, 'p' },
 		{ "real", required_argument, NULL, 'r' },
@@ -171,6 +172,7 @@ int main(int argc, char *argv[]) {
 			break; case 'h': host = optarg;
 			break; case 'j': self.join = optarg;
 			break; case 'k': priv = optarg;
+			break; case 'l': logEnable = true;
 			break; case 'n': nick = optarg;
 			break; case 'p': port = optarg;
 			break; case 'r': real = optarg;
@@ -327,5 +329,6 @@ int main(int argc, char *argv[]) {
 	handle(msg);
 
 	ircClose();
+	logClose();
 	uiHide();
 }
diff --git a/chat.h b/chat.h
index 7ffcfcd..0a84053 100644
--- a/chat.h
+++ b/chat.h
@@ -259,8 +259,14 @@ void urlOpenCount(uint id, uint count);
 void urlOpenMatch(uint id, const char *str);
 void urlCopyMatch(uint id, const char *str);
 
+extern bool logEnable;
+void logFormat(uint id, const time_t *time, const char *format, ...)
+	__attribute__((format(printf, 3, 4)));
+void logClose(void);
+
 FILE *configOpen(const char *path, const char *mode);
 FILE *dataOpen(const char *path, const char *mode);
+void dataMkdir(const char *path);
 
 int getopt_config(
 	int argc, char *const *argv,
diff --git a/log.c b/log.c
new file mode 100644
index 0000000..7f99ec4
--- /dev/null
+++ b/log.c
@@ -0,0 +1,112 @@
+/* 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 <assert.h>
+#include <err.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sysexits.h>
+#include <time.h>
+
+#include "chat.h"
+
+bool logEnable;
+
+static struct {
+	int year;
+	int month;
+	int day;
+	FILE *file;
+} logs[IDCap];
+
+static FILE *logFile(uint id, const struct tm *tm) {
+	if (
+		logs[id].file &&
+		logs[id].year == tm->tm_year &&
+		logs[id].month == tm->tm_mon &&
+		logs[id].day == tm->tm_mday
+	) return logs[id].file;
+
+	if (logs[id].file) {
+		int error = fclose(logs[id].file);
+		if (error) err(EX_IOERR, "%s", idNames[id]);
+	}
+
+	logs[id].year = tm->tm_year;
+	logs[id].month = tm->tm_mon;
+	logs[id].day = tm->tm_mday;
+
+	char path[PATH_MAX] = "log";
+	size_t len = strlen(path);
+	dataMkdir("");
+	dataMkdir(path);
+
+	path[len++] = '/';
+	for (const char *ch = network.name; *ch; ++ch) {
+		path[len++] = (*ch == '/' ? '_' : *ch);
+	}
+	path[len] = '\0';
+	dataMkdir(path);
+
+	path[len++] = '/';
+	for (const char *ch = idNames[id]; *ch; ++ch) {
+		path[len++] = (*ch == '/' ? '_' : *ch);
+	}
+	path[len] = '\0';
+	dataMkdir(path);
+
+	strftime(&path[len], sizeof(path) - len, "/%F.log", tm);
+	logs[id].file = dataOpen(path, "a");
+	if (!logs[id].file) exit(EX_CANTCREAT);
+
+	setlinebuf(logs[id].file);
+	return logs[id].file;
+}
+
+void logClose(void) {
+	if (!logEnable) return;
+	for (uint id = 0; id < IDCap; ++id) {
+		if (!logs[id].file) continue;
+		int error = fclose(logs[id].file);
+		if (error) err(EX_IOERR, "%s", idNames[id]);
+	}
+}
+
+void logFormat(uint id, const time_t *src, const char *format, ...) {
+	if (!logEnable) return;
+
+	time_t ts = (src ? *src : time(NULL));
+	struct tm *tm = localtime(&ts);
+	if (!tm) err(EX_OSERR, "localtime");
+
+	FILE *file = logFile(id, tm);
+
+	char buf[sizeof("0000-00-00T00:00:00+0000")];
+	strftime(buf, sizeof(buf), "%FT%T%z", tm);
+	fprintf(file, "[%s] ", buf);
+	if (ferror(file)) err(EX_IOERR, "%s", idNames[id]);
+
+	va_list ap;
+	va_start(ap, format);
+	vfprintf(file, format, ap);
+	va_end(ap);
+	if (ferror(file)) err(EX_IOERR, "%s", idNames[id]);
+
+	fprintf(file, "\n");
+	if (ferror(file)) err(EX_IOERR, "%s", idNames[id]);
+}
diff --git a/xdg.c b/xdg.c
index ed2a6e1..c70873a 100644
--- a/xdg.c
+++ b/xdg.c
@@ -134,3 +134,25 @@ local:
 	if (!file) warn("%s", path);
 	return file;
 }
+
+void dataMkdir(const char *path) {
+	const char *home = getenv("HOME");
+	const char *dataHome = getenv("XDG_DATA_HOME");
+
+	char homePath[PATH_MAX];
+	if (dataHome) {
+		snprintf(
+			homePath, sizeof(homePath),
+			"%s/" SUBDIR "/%s", dataHome, path
+		);
+	} else {
+		if (!home) return;
+		snprintf(
+			homePath, sizeof(homePath),
+			"%s/.local/share/" SUBDIR "/%s", home, path
+		);
+	}
+
+	int error = mkdir(homePath, S_IRWXU);
+	if (error && errno != EEXIST) warn("%s", homePath);
+}