hist.c (12092B)
1 /* 2 * src/hist.c from hirc 3 * 4 * Copyright (c) 2021-2022 hhvn <dev@hhvn.uk> 5 * 6 * Permission to use, copy, modify, and distribute this software for any 7 * purpose with or without fee is hereby granted, provided that the above 8 * copyright notice and this permission notice appear in all copies. 9 * 10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 * 18 */ 19 20 #include <stdio.h> 21 #include <fcntl.h> 22 #include <errno.h> 23 #include <regex.h> 24 #include <stdlib.h> 25 #include <unistd.h> 26 #include <string.h> 27 #include <stdarg.h> 28 #include <stdlib.h> 29 #include <ncurses.h> 30 #include <sys/stat.h> 31 #include "hirc.h" 32 33 void 34 hist_free(struct History *history) { 35 param_free(history->_params); 36 nick_free(history->from); 37 pfree(&history->raw); 38 pfree(&history->format); 39 pfree(&history->rformat); 40 pfree(&history); 41 } 42 43 void 44 hist_free_list(struct HistInfo *histinfo) { 45 struct History *p, *prev; 46 47 if (!histinfo->history) 48 return; 49 50 prev = histinfo->history; 51 p = prev->next; 52 while (prev) { 53 hist_free(prev); 54 prev = p; 55 if (p) 56 p = p->next; 57 } 58 histinfo->history = NULL; 59 } 60 61 struct History * 62 hist_create(struct HistInfo *histinfo, struct Nick *from, char *msg, 63 enum Activity activity, time_t timestamp, enum HistOpt options) { 64 struct History *new; 65 struct Nick *np; 66 char *nick; 67 68 assert_warn(msg, NULL); 69 70 new = emalloc(sizeof(struct History)); 71 new->prev = new->next = NULL; 72 new->timestamp = timestamp ? timestamp : time(NULL); 73 new->activity = activity; 74 new->raw = estrdup(msg); 75 new->_params = new->params = param_create(msg); 76 new->rformat = new->format = NULL; 77 new->options = options; 78 new->origin = histinfo; 79 80 if (from) { 81 new->from = nick_dup(from); 82 } else if (**new->_params == ':') { 83 np = NULL; 84 if (histinfo->channel && histinfo->channel->nicks) { 85 prefix_tokenize(*new->_params, &nick, NULL, NULL); 86 np = nick_get(&histinfo->channel->nicks, nick); 87 free(nick); 88 } 89 90 if (np) 91 new->from = nick_dup(np); 92 else 93 new->from = nick_create(*new->_params, ' ', histinfo->server); 94 } else { 95 new->from = NULL; 96 } 97 98 /* Update histinfo->server->self */ 99 if (new->from && new->from->self && histinfo->server) { 100 if (new->from->ident && strcmp_n(new->from->ident, histinfo->server->self->ident) != 0) { 101 free(histinfo->server->self->ident); 102 histinfo->server->self->ident = strdup(new->from->ident); 103 } 104 if (new->from->host && strcmp_n(new->from->host, histinfo->server->self->host) != 0) { 105 free(histinfo->server->self->host); 106 histinfo->server->self->host = strdup(new->from->host); 107 } 108 } 109 110 if (**new->_params == ':') 111 new->params++; 112 113 return new; 114 } 115 116 struct History * 117 hist_addp(struct HistInfo *histinfo, struct History *p, enum Activity activity, enum HistOpt options) { 118 return hist_add(histinfo, p->raw, activity, p->timestamp, options); 119 } 120 121 struct History * 122 hist_add(struct HistInfo *histinfo, 123 char *msg, enum Activity activity, 124 time_t timestamp, enum HistOpt options) { 125 struct Nick *from = NULL; 126 struct History *new, *p; 127 struct Ignore *ign; 128 struct tm ptm, ctm; 129 int i; 130 131 assert_warn(histinfo && msg, NULL); 132 133 if (options & HIST_MAIN) { 134 if (options & HIST_TMP && histinfo == main_buf) { 135 hist_add(main_buf, msg, activity, timestamp, options & ~(HIST_MAIN|HIST_TMP|HIST_LOG)); 136 new = NULL; 137 goto ui; 138 } else if (histinfo != main_buf) { 139 hist_add(main_buf, msg, activity, timestamp, options & ~(HIST_MAIN|HIST_TMP|HIST_LOG)); 140 } else { 141 ui_error("HIST_MAIN specified, but history is &main_buf", NULL); 142 } 143 } 144 145 if (options & HIST_SELF && histinfo->server) { 146 if (histinfo->channel && histinfo->channel->nicks) 147 from = nick_get(&histinfo->channel->nicks, histinfo->server->self->nick); 148 if (!from) 149 from = histinfo->server->self; 150 } 151 152 new = hist_create(histinfo, from, msg, activity, timestamp, options); 153 154 if (!(options & HIST_NIGN)) { 155 for (ign = ignores; ign; ign = ign->next) { 156 if ((!ign->server || 157 (histinfo->server && strcmp_n(ign->server, histinfo->server->name) == 0)) && 158 (!ign->format || strcmp_n(format_get(new), ign->format) == 0) && 159 regexec(&ign->regex, msg, 0, NULL, 0) == 0) { 160 if (!ign->noact) { 161 options |= HIST_IGN; 162 new->options = options; 163 } 164 activity = Activity_none; 165 } 166 } 167 } 168 169 if (strcmp(msg, "SELF_NEW_DAY") != 0 && 170 histinfo && histinfo->history && 171 histinfo->history->timestamp < timestamp && 172 !(histinfo->history->options & HIST_RLOG) && 173 !(histinfo->history->options & HIST_GREP) && 174 !(options & HIST_GREP)) { 175 localtime_r(&histinfo->history->timestamp, &ptm); 176 localtime_r(×tamp, &ctm); 177 if (ptm.tm_mday != ctm.tm_mday || ptm.tm_mon != ctm.tm_mon || ptm.tm_year != ctm.tm_year) { 178 ctm.tm_sec = ctm.tm_min = ctm.tm_hour = 0; 179 hist_format(histinfo, Activity_none, histinfo->server ? HIST_DFL : HIST_SHOW, 180 "SELF_NEW_DAY %lld :day changed to", (long long)mktime(&ctm)); 181 histinfo->history->timestamp = mktime(&ctm); /* set timestamp of SELF_NEW_DAY: 182 really hist_format should take a timestamp */ 183 } 184 } 185 186 if (!histinfo->history) { 187 histinfo->history = new; 188 goto ui; 189 } 190 191 for (i=0, p = histinfo->history; p && p->next; p = p->next, i++); 192 if (i == (HIST_MAX-1)) { 193 pfree(&p->next); 194 p->next = NULL; 195 } 196 197 new->next = histinfo->history; 198 new->next->prev = new; 199 histinfo->history = new; 200 201 ui: 202 if (options & HIST_SHOW && 203 activity >= Activity_hilight && 204 config_getl("misc.bell")) 205 beep(); 206 207 if (histinfo && options & HIST_SHOW && 208 activity > histinfo->activity && 209 histinfo != selected.history) { 210 histinfo->activity = activity; 211 windows[Win_buflist].refresh = 1; 212 } 213 214 if (histinfo && options & HIST_SHOW && histinfo != selected.history) { 215 if (options & HIST_IGN) 216 histinfo->ignored++; 217 else 218 histinfo->unread++; 219 } 220 221 if (options & HIST_LOG) { 222 if (histinfo->server) 223 hist_log(new); 224 else 225 ui_error("HIST_LOG specified, but server is NULL", NULL); 226 } 227 228 /* TODO: this triggers way too often, need to have some sort of delay */ 229 if (selected.history == histinfo) { 230 if (options & HIST_SELF) 231 windows[Win_main].scroll = -1; 232 else if (windows[Win_main].scroll >= 0) 233 windows[Win_main].scroll += 1; 234 windows[Win_main].refresh = 1; 235 } 236 237 return new; 238 } 239 240 void 241 hist_purgeopt(struct HistInfo *histinfo, enum HistOpt options) { 242 struct History *p, *next; 243 244 assert_warn(histinfo,); 245 246 p = histinfo->history; 247 248 for (; p; p = next) { 249 next = p->next; 250 if (p->options & options) { 251 if (p->prev) 252 p->prev->next = p->next; 253 else 254 histinfo->history = p->next; 255 256 if (p->next) 257 p->next->prev = p->prev; 258 else if (!p->prev) 259 histinfo->history = NULL; 260 261 pfree(&p); 262 } 263 } 264 } 265 266 struct History * 267 hist_format(struct HistInfo *histinfo, enum Activity activity, enum HistOpt options, char *format, ...) { 268 char msg[1024]; 269 va_list ap; 270 271 va_start(ap, format); 272 vsnprintf(msg, sizeof(msg), format, ap); 273 va_end(ap); 274 275 if (histinfo) 276 return hist_add(histinfo, msg, Activity_status, 0, options); 277 else 278 return hist_create(histinfo, NULL, msg, Activity_status, 0, options); 279 } 280 281 int 282 hist_log(struct History *hist) { 283 char filename[2048]; 284 FILE *f; 285 char *logdir; 286 int ret; 287 struct stat st; 288 char *nick, *ident, *host, *raw; 289 290 if (!config_getl("log.toggle")) 291 return -2; 292 293 if ((logdir = config_gets("log.dir")) == NULL) 294 return -3; 295 296 logdir = homepath(logdir); 297 298 if (!hist || !hist->origin || !hist->origin->server) 299 return -4; 300 301 if (stat(logdir, &st) == -1) { 302 if (mkdir(logdir, 0700) == -1) { 303 ui_error("Could not create dir '%s': %s", logdir, strerror(errno)); 304 return -5; 305 } 306 } 307 308 /* use ',' as it's illegal in dns hostnames (I think) and channel names */ 309 if (hist->origin->channel) 310 snprintf(filename, sizeof(filename), "%s/%s,%s.log", logdir, hist->origin->server->name, hist->origin->channel->name); 311 else 312 snprintf(filename, sizeof(filename), "%s/%s.log", logdir, hist->origin->server->name); 313 314 if (!(f = fopen(filename, "a"))) { 315 ui_error("Could not open '%s': %s", filename, strerror(errno)); 316 return -6; 317 } 318 319 if (hist->from) { 320 nick = hist->from->nick ? hist->from->nick : " "; 321 ident = hist->from->ident ? hist->from->ident : " "; 322 host = hist->from->host ? hist->from->host : " "; 323 } else { 324 nick = ident = host = " "; 325 } 326 327 if (*hist->raw == ':' && strchr(hist->raw, ' ')) 328 raw = strchr(hist->raw, ' ') + 1; 329 else 330 raw = hist->raw; 331 332 ret = fprintf(f, 333 "v2\t%lld\t%d\t%d\t%d\t%c\t%s\t%s\t%s\t%s\n", 334 (long long)hist->timestamp, 335 hist->activity, 336 hist->options, /* write all options - only options ANDing with HIST_LOGACCEPT are read later */ 337 hist->from ? hist->from->self : 0, /* If from does not exist, it's probably not from us */ 338 hist->from ? hist->from->priv : ' ', 339 nick, ident, host, raw); 340 341 if (ret < 0) { 342 ui_error("Could not write to '%s': %s", filename, strerror(errno)); 343 fclose(f); 344 return -7; 345 } 346 347 fclose(f); 348 return 0; 349 } 350 351 struct History * 352 hist_loadlog(struct HistInfo *hist, char *server, char *channel) { 353 struct History *head = NULL, *p, *prev; 354 struct stat st; 355 char filename[2048]; 356 char *logdir; 357 FILE *f; 358 char *lines[HIST_MAX]; 359 char buf[2048]; 360 int i, j; 361 char *version; 362 char *tok[8]; 363 char *msg; 364 time_t timestamp; 365 enum Activity activity; 366 enum HistOpt options; 367 char *prefix; 368 size_t len; 369 struct Nick *from; 370 371 assert_warn(server && hist, NULL); 372 373 if ((logdir = config_gets("log.dir")) == NULL) 374 return NULL; 375 376 logdir = homepath(logdir); 377 378 if (channel) 379 snprintf(filename, sizeof(filename), "%s/%s,%s.log", logdir, server, channel); 380 else 381 snprintf(filename, sizeof(filename), "%s/%s.log", logdir, server); 382 383 if (stat(filename, &st) == -1) 384 return NULL; 385 386 if (!(f = fopen(filename, "rb"))) 387 return NULL; 388 389 memset(lines, 0, sizeof(lines)); 390 391 while (fgets(buf, sizeof(buf), f)) { 392 pfree(&lines[HIST_MAX - 1]); 393 memmove(lines + 1, lines, HIST_MAX - 1); 394 buf[strlen(buf) - 1] = '\0'; /* strip newline */ 395 lines[0] = estrdup(buf); 396 } 397 398 for (i = 0, prev = NULL; i < HIST_MAX && lines[i]; i++) { 399 if (*lines[i] == 'v') 400 version = strtok_r(lines[i], "\t", &msg) + 1; /* in future versioning could allow for back-compat */ 401 else 402 version = NULL; 403 tok[0] = strtok_r(*lines[i] == 'v' ? NULL : lines[i], "\t", &msg); 404 for (j = 1; j < (sizeof(tok) / sizeof(tok[0])); j++) 405 tok[j] = strtok_r(NULL, "\t", &msg); /* strtok_r will store remaining text after the tokens in msg. 406 * This is used instead of a tok[8] as messages can contain tabs. */ 407 408 if (!tok[0] || !tok[1] || !tok[2] || 409 !tok[3] || !tok[4] || !tok[5] || 410 !tok[6] || !tok[7] || !msg) { 411 pfree(&lines[i]); 412 continue; 413 } 414 415 timestamp = (time_t)strtoll(tok[0], NULL, 10); 416 activity = (int)strtol(tok[1], NULL, 10); 417 options = HIST_RLOG|(strtol(tok[2], NULL, 10) & HIST_LOGACCEPT); 418 419 len = 1; 420 if (*tok[5] != ' ') 421 len += strlen(tok[5]); 422 if (*tok[6] != ' ') 423 len += strlen(tok[6]) + 1; 424 if (*tok[7] != ' ') 425 len += strlen(tok[7]) + 1; 426 prefix = smprintf(len, "%s%s%s%s%s", 427 tok[5] ? tok[5] : "", 428 tok[6] ? "!" : "", tok[6] ? tok[6] : "", 429 tok[7] ? "@" : "", tok[7] ? tok[7] : ""); 430 from = nick_create(prefix, *tok[4], hist->server); 431 if (from) 432 from->self = *tok[3] == '1'; 433 434 p = hist_create(hist, from, msg, activity, timestamp, options); 435 436 if (!head) 437 head = p; 438 439 if (prev) { 440 prev->next = p; 441 p->prev = prev; 442 } 443 prev = p; 444 445 nick_free(from); 446 pfree(&prefix); 447 pfree(&lines[i]); 448 } 449 450 fclose(f); 451 452 if (head) { 453 p = hist_format(NULL, Activity_none, HIST_SHOW|HIST_RLOG, "SELF_LOG_RESTORE %lld :log restored up to", (long long)st.st_mtime); 454 p->origin = hist; 455 p->next = head; 456 head->prev = p; 457 head = p; 458 } 459 return head; 460 } 461 462 int 463 hist_len(struct History **history) { 464 struct History *p; 465 int i; 466 467 for (i=0, p = *history; p; p = p->next) 468 i++; 469 return i; 470 }