diff --git a/Makefile b/Makefile index a1044d3..5f30bb4 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,18 @@ CFLAGS += $(LIBRESSL_PREFIX:%=-I%/include) LDFLAGS += $(LIBRESSL_PREFIX:%=-L%/lib) LDLIBS = -lcursesw -ltls -OBJS = chat.o edit.o handle.o input.o irc.o pls.o tab.o tag.o term.o ui.o url.o +OBJS += chat.o +OBJS += edit.o +OBJS += handle.o +OBJS += input.o +OBJS += irc.o +OBJS += log.o +OBJS += pls.o +OBJS += tab.o +OBJS += tag.o +OBJS += term.o +OBJS += ui.o +OBJS += url.o all: tags chat diff --git a/chat.c b/chat.c index 9d5a323..050a4f0 100644 --- a/chat.c +++ b/chat.c @@ -161,11 +161,12 @@ int main(int argc, char *argv[]) { const char *webirc = NULL; int opt; - while (0 < (opt = getopt(argc, argv, "W:h:j:n:p:u:vw:"))) { + while (0 < (opt = getopt(argc, argv, "W:h:j:l:n:p:u:vw:"))) { switch (opt) { break; case 'W': webirc = optarg; break; case 'h': host = strdup(optarg); break; case 'j': selfJoin(optarg); + break; case 'l': logOpen(optarg); break; case 'n': selfNick(optarg); break; case 'p': port = optarg; break; case 'u': selfUser(optarg); diff --git a/chat.h b/chat.h index c66683b..51494de 100644 --- a/chat.h +++ b/chat.h @@ -154,6 +154,11 @@ void urlScan(struct Tag tag, const char *str); void urlList(struct Tag tag); void urlOpen(struct Tag tag, size_t at, size_t to); +void logOpen(const char *path); +void logFmt( + struct Tag tag, const time_t *ts, const char *format, ... +) __attribute__((format(printf, 3, 4))); + void spawn(char *const argv[]); wchar_t *ambstowcs(const char *src); diff --git a/handle.c b/handle.c index 71fab12..89d101a 100644 --- a/handle.c +++ b/handle.c @@ -166,6 +166,7 @@ static void handleJoin(char *prefix, char *params) { "\3%d%s\3 arrives in \3%d%s\3", color(user), nick, color(chan), chan ); + logFmt(tag, NULL, "%s arrives in %s", nick, chan); } static void handlePart(char *prefix, char *params) { @@ -184,14 +185,16 @@ static void handlePart(char *prefix, char *params) { uiFmt( tag, UI_COLD, "\3%d%s\3 leaves \3%d%s\3, \"%s\"", - color(user), nick, color(chan), chan, mesg + color(user), nick, color(chan), chan, dequote(mesg) ); + logFmt(tag, NULL, "%s leaves %s, \"%s\"", nick, chan, dequote(mesg)); } else { uiFmt( tag, UI_COLD, "\3%d%s\3 leaves \3%d%s\3", color(user), nick, color(chan), chan ); + logFmt(tag, NULL, "%s leaves %s", nick, chan); } } @@ -215,12 +218,17 @@ static void handleKick(char *prefix, char *params) { color(user), nick, color(kick), kick, color(chan), chan, dequote(mesg) ); + logFmt( + tag, NULL, + "%s kicks %s out of %s, \"%s\"", nick, kick, chan, dequote(mesg) + ); } else { uiFmt( tag, (kicked ? UI_HOT : UI_COLD), "\3%d%s\3 kicks \3%d%s\3 out of \3%d%s\3", color(user), nick, color(kick), kick, color(chan), chan ); + logFmt(tag, NULL, "%s kicks %s out of %s", nick, kick, chan); } } @@ -239,8 +247,10 @@ static void handleQuit(char *prefix, char *params) { "\3%d%s\3 leaves, \"%s\"", color(user), nick, dequote(mesg) ); + logFmt(tag, NULL, "%s leaves, \"%s\"", nick, dequote(mesg)); } else { uiFmt(tag, UI_COLD, "\3%d%s\3 leaves", color(user), nick); + logFmt(tag, NULL, "%s leaves", nick); } } } @@ -257,6 +267,7 @@ static void handleReplyTopic(char *prefix, char *params) { "The sign in \3%d%s\3 reads, \"%s\"", color(chan), chan, topic ); + logFmt(tag, NULL, "The sign in %s reads, \"%s\"", chan, topic); } static void handleTopic(char *prefix, char *params) { @@ -273,6 +284,7 @@ static void handleTopic(char *prefix, char *params) { "\3%d%s\3 places a new sign in \3%d%s\3, \"%s\"", color(user), nick, color(chan), chan, topic ); + logFmt(tag, NULL, "%s places a new sign in %s, \"%s\"", nick, chan, topic); } static void handleReplyEndOfNames(char *prefix, char *params) { @@ -335,6 +347,7 @@ static void handleNick(char *prefix, char *params) { "\3%d%s\3 is now known as \3%d%s\3", color(user), prev, color(user), next ); + logFmt(tag, NULL, "%s is now known as %s", prev, next); } } @@ -354,6 +367,7 @@ static void handleCTCP(struct Tag tag, char *nick, char *user, char *mesg) { "%c\3%d* %s\17 %s", ping["\17\26"], color(user), nick, params ); + logFmt(tag, NULL, "* %s %s", nick, params); } static void handlePrivmsg(char *prefix, char *params) { @@ -375,6 +389,7 @@ static void handlePrivmsg(char *prefix, char *params) { "%c\3%d%c%s%c\17 %s", ping["\17\26"], color(user), self["<("], nick, self[">)"], mesg ); + logFmt(tag, NULL, "<%s> %s", nick, mesg); } static void handleNotice(char *prefix, char *params) { @@ -393,6 +408,7 @@ static void handleNotice(char *prefix, char *params) { "%c\3%d-%s-\17 %s", ping["\17\26"], color(user), nick, mesg ); + logFmt(tag, NULL, "-%s- %s", nick, mesg); } static const struct { diff --git a/log.c b/log.c new file mode 100644 index 0000000..2a47806 --- /dev/null +++ b/log.c @@ -0,0 +1,121 @@ +/* Copyright (C) 2018 Curtis McEnroe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "chat.h" + +static int logRoot = -1; + +static struct Log { + int dir; + int year; + int month; + int day; + FILE *file; +} logs[TAGS_LEN]; + +void logOpen(const char *path) { + logRoot = open(path, O_RDONLY | O_CLOEXEC); + if (logRoot < 0) err(EX_CANTCREAT, "%s", path); +} + +static void sanitize(char *name) { + for (; name[0]; ++name) { + if (name[0] == '/') name[0] = '_'; + } +} + +static FILE *logFile(struct Tag tag, const struct tm *time) { + struct Log *log = &logs[tag.id]; + if ( + log->file + && log->year == time->tm_year + && log->month == time->tm_mon + && log->day == time->tm_mday + ) return log->file; + + if (log->file) { + fclose(log->file); + + } else { + char *name = strdup(tag.name); + if (!name) err(EX_OSERR, "strdup"); + sanitize(name); + + int error = mkdirat(logRoot, name, 0700); + if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", name); + + log->dir = openat(logRoot, name, O_RDONLY | O_CLOEXEC); + if (log->dir < 0) err(EX_CANTCREAT, "%s", name); + + free(name); + } + + log->year = time->tm_year; + log->month = time->tm_mon; + log->day = time->tm_mday; + + char path[sizeof("YYYY-MM-DD.log")]; + strftime(path, sizeof(path), "%F.log", time); + int fd = openat( + log->dir, path, O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, 0700 + ); + if (fd < 0) err(EX_CANTCREAT, "%s/%s", tag.name, path); + + log->file = fdopen(fd, "a"); + if (!log->file) err(EX_CANTCREAT, "%s/%s", tag.name, path); + setlinebuf(log->file); + + return log->file; +} + +void logFmt(struct Tag tag, const time_t *ts, const char *format, ...) { + if (logRoot < 0) return; + + time_t t; + if (!ts) { + t = time(NULL); + ts = &t; + } + + struct tm *time = localtime(ts); + if (!time) err(EX_SOFTWARE, "localtime"); + + FILE *file = logFile(tag, time); + + char stamp[sizeof("YYYY-MM-DDThh:mm:ss+hhmm")]; + strftime(stamp, sizeof(stamp), "%FT%T%z", time); + fprintf(file, "[%s] ", stamp); + if (ferror(file)) err(EX_IOERR, "%s", tag.name); + + va_list ap; + va_start(ap, format); + vfprintf(file, format, ap); + va_end(ap); + if (ferror(file)) err(EX_IOERR, "%s", tag.name); + + fprintf(file, "\n"); + if (ferror(file)) err(EX_IOERR, "%s", tag.name); +}