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 + * + * 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 . + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#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); +}