Split up config structs.
[jelmer/ctrlproxy.git] / mods / log_custom.c
1 /* 
2         ctrlproxy: A modular IRC proxy
3         (c) 2002-2003 Jelmer Vernooij <jelmer@nl.linux.org>
4
5         This program is free software; you can redistribute it and/or modify
6         it under the terms of the GNU General Public License as published by
7         the Free Software Foundation; either version 2 of the License, or
8         (at your option) any later version.
9
10         This program is distributed in the hope that it will be useful,
11         but WITHOUT ANY WARRANTY; without even the implied warranty of
12         MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13         GNU General Public License for more details.
14
15         You should have received a copy of the GNU General Public License
16         along with this program; if not, write to the Free Software
17         Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 */
19
20 #include "ctrlproxy.h"
21 #include <stdio.h>
22 #include <stdarg.h>
23 #include <string.h>
24 #include <stdlib.h>
25 #include <stdio.h>
26 #include <time.h>
27 #include <glib.h>
28 #include <sys/stat.h>
29 #include <sys/types.h>
30
31 #ifdef _WIN32
32 #include <direct.h>
33 #define mkdir(s,t) _mkdir(s)
34 #endif
35
36
37 #define MAX_SUBST 256
38 #undef G_LOG_DOMAIN
39 #define G_LOG_DOMAIN "log_custom"
40
41 const char *logfilename = NULL;
42 GHashTable *fmts = NULL;
43
44 /* Translation table */
45 struct log_mapping {
46         char *command;
47         char subst;
48         unsigned int index;
49         /* If index is -1 */
50         char *(*callback) (struct network *, struct line *l, gboolean case_sensitive);
51 };
52
53 static char *get_hours(struct network *n, struct line *l, gboolean case_sensitive) { 
54         time_t ti = time(NULL);
55         struct tm *t = localtime(&ti);
56         return g_strdup_printf("%02d", t->tm_hour);
57 }
58
59 static char *get_minutes(struct network *n, struct line *l, gboolean case_sensitive) { 
60         time_t ti = time(NULL);
61         struct tm *t = localtime(&ti);
62         return g_strdup_printf("%02d", t->tm_min);
63 }
64
65 static char *get_seconds(struct network *n, struct line *l, gboolean case_sensitive) { 
66         time_t ti = time(NULL);
67         struct tm *t = localtime(&ti);
68         return g_strdup_printf("%02d", t->tm_sec);
69 }
70
71 static char *get_seconds_since_1970(struct network *n, struct line *l, gboolean case_sensitive) {
72         time_t ti = time(NULL);
73         return g_strdup_printf("%ld", ti);
74 }
75
76 static char *get_day(struct network *n, struct line *l, gboolean case_sensitive) { 
77         time_t ti = time(NULL);
78         struct tm *t = localtime(&ti);
79         return g_strdup_printf("%02d", t->tm_mday);
80 }
81
82 static char *get_user(struct network *n, struct line *l, gboolean case_sensitive) {
83         char *nick = NULL;
84         char *user = NULL;
85
86         if(l->origin)nick = g_strdup(l->origin);
87         if(nick)user = strchr(nick, '!');
88         if(user){ *user = '\0';user++; }
89
90         if(case_sensitive) return g_ascii_strdown(user, -1);
91         else return g_strdup(user);
92 }
93
94 static char *get_monthname(struct network *n, struct line *l, gboolean case_sensitive) { 
95         char stime[512];
96         time_t ti = time(NULL);
97         strftime(stime, sizeof(stime), "%b", localtime(&ti));
98         return g_strdup_printf("%s", stime);
99 }
100
101 static char *get_nick(struct network *n, struct line *l, gboolean case_sensitive) {
102         if(line_get_nick(l)) {
103                 if(case_sensitive) return g_ascii_strdown(line_get_nick(l), -1);
104                 else return g_strdup(line_get_nick(l)); 
105         }
106         
107         return g_strdup("");
108 }
109
110 static char *get_network(struct network *n, struct line *l, gboolean case_sensitive) 
111 { return g_strdup(n->name); }
112 static char *get_server(struct network *n, struct line *l, gboolean case_sensitive)
113 { return g_strdup(n->connection.data.tcp.current_server->name); }
114
115 static char *get_percent(struct network *n, struct line *l, gboolean case_sensitive) { return g_strdup("%"); }
116
117 static const char *identifier = NULL;
118
119 static char *get_identifier(struct network *n, struct line *l, gboolean case_sensitive) { 
120         if(case_sensitive) return g_ascii_strdown(identifier, -1); 
121         else return g_strdup(identifier); 
122 }
123
124 static char *get_modechanges(struct network *n, struct line *l, gboolean case_sensitive) {
125         char buf[512] = "";
126         int i;
127
128         for( i = 3 ; l->args[i+1] != NULL; i++ )
129                 if(i == 3) sprintf(buf, "%s", l->args[i]);
130                 else sprintf(buf, "%s %s", buf, l->args[i]);
131
132         return g_strdup(buf);
133 }
134
135 static struct log_mapping mappings[] = {
136         {NULL, '@', -1, get_identifier },
137         {NULL, 'h', -1, get_hours },
138         {NULL, 'M', -1, get_minutes },
139         {NULL, 's', -1, get_seconds },
140         {NULL, 'd', -1, get_day },
141     {NULL, 'e', -1, get_seconds_since_1970 },
142         {NULL, 'b', -1, get_monthname },
143         {NULL, 'n', -1, get_nick },
144         {NULL, 'u', -1, get_user },
145         {NULL, 'N', -1, get_network },
146         {NULL, 'S', -1, get_server },
147         {NULL, '%', -1, get_percent },
148         {"JOIN", 'c', 1, NULL },
149         {"PART", 'c', 1, NULL },
150         {"PART", 'm', 2, NULL },
151         {"KICK", 'c', 1, NULL },
152         {"KICK", 't', 2, NULL },
153         {"KICK", 'r', 3, NULL },
154         {"QUIT", 'm', 1, NULL },
155         {"NOTICE", 't', 1, NULL },
156         {"NOTICE", 'm', 2, NULL },
157         {"PRIVMSG", 't', 1, NULL },
158         {"PRIVMSG", 'm', 2, NULL },
159         {"MSG", 't', 1, NULL },
160         {"MSG", 'm', 2, NULL },
161         {"TOPIC", 'c', 1, NULL },
162         {"TOPIC", 't', 2, NULL },
163         {"MODE", 't', 1, NULL },
164         {"MODE", 'p', 2, NULL },
165         {"MODE", 'c', -1, get_modechanges },
166         {"NICK", 'r', 1, NULL },
167         {NULL, '0', 0, NULL },
168         {NULL, '1', 1, NULL },
169         {NULL, '2', 2, NULL },
170         {NULL, '3', 3, NULL },
171         {NULL, '4', 4, NULL },
172         {NULL, '5', 5, NULL },
173         {NULL, '6', 6, NULL },
174         {NULL, '7', 7, NULL },
175         {NULL, '8', 8, NULL },
176         {NULL, '9', 9, NULL },
177         { NULL }
178 };
179
180 static char *find_mapping(struct network *network, struct line *l, char c, gboolean case_sensitive)
181 {
182         int i;
183         for(i = 0; mappings[i].subst; i++) {
184                 if(mappings[i].command && 
185                    strcmp(mappings[i].command, l->args[0])) continue;
186                 if(mappings[i].subst != c)continue;
187                 if(mappings[i].index == -1) return mappings[i].callback(network, l, case_sensitive);
188                 if(mappings[i].index < l->argc) {
189                         if(case_sensitive) return g_ascii_strdown(l->args[mappings[i].index], -1);
190                         else return g_strdup(l->args[mappings[i].index]);
191                 }
192         }
193         return g_strdup("");
194 }
195
196 static void custom_subst(struct network *network, char **_new, const char *fmt, struct line *l, const char *_identifier, gboolean case_sensitive)
197 {
198         char *subst[MAX_SUBST];
199         char *new;
200         size_t len, curpos = 0;
201         unsigned int i;
202
203         identifier = _identifier;
204
205         len = strlen(fmt);
206         memset(subst, 0, sizeof(char *) * MAX_SUBST);
207         for(i = 0; i < strlen(fmt); i++) {
208                 if(fmt[i] == '%') {
209                         subst[(int)fmt[i+1]] = find_mapping(network, l, fmt[i+1], case_sensitive);      
210                         if (subst[(int)fmt[i+1]] == NULL) subst[(int)fmt[i+1]] = g_strdup("");
211                         len += strlen(subst[(int)fmt[i+1]]);
212                 }
213         }
214
215         len++; /* newline! */
216
217         new = g_new(char, len);
218         for(i = 0; i < strlen(fmt); i++) {
219                 if(fmt[i] == '%') {
220                         new[curpos] = '\0';
221                         strncat(new, subst[(int)fmt[i+1]], len);
222                         curpos+=strlen(subst[(int)fmt[i+1]]);
223                         i++;
224                 } else {
225                         new[curpos] = fmt[i];
226                         curpos++;
227                 }
228         }
229         new[curpos] = '\0';
230
231         for(i = 0; i < MAX_SUBST; i++) { if(subst[i])g_free(subst[i]); }
232         *_new = new;
233 }
234
235 /* Syntax:
236 Always:
237  * %h -> hours
238  * %m -> minutes
239  * %s -> seconds
240  * %d -> day
241  * %b -> locale month name
242  * %n -> initiating nick
243  * %u -> initiating user
244  * %N -> network name
245  * %S -> server name
246  * %% -> percent sign
247 If appropriate:
248  * %c -> channel name
249  * %m -> message
250
251  -- JOIN: %c
252  -- PART: %c, %m
253  -- KICK: %t (target nick), %r (reason)
254  -- QUIT: %m
255  -- NOTICE/PRIVMSG: %t (target nick/channel), %m
256  -- MODE: %p(mode change), %t, %c (target nicks)
257  -- TOPIC: %t
258  -- NICK: %r
259  */
260
261 static GHashTable *files = NULL;
262
263 static FILE *find_channel_file(struct network *network, struct line *l, char *identifier) {
264         char *n = NULL;
265         FILE *f;
266         if(!logfilename)return NULL;
267         custom_subst(network, &n, logfilename, l, identifier, TRUE);
268         f = g_hash_table_lookup(files, n);
269         g_free(n);
270         return f;
271 }
272
273 static FILE *find_add_channel_file(struct network *network, struct line *l, const char *identifier) 
274 {
275         char *n = NULL, *dn, *p;
276         FILE *f;
277         if(!logfilename) return NULL;
278         custom_subst(network, &n, logfilename, l, identifier, TRUE);
279         f = g_hash_table_lookup(files, n);
280         if(!f) {
281                 dn = g_strdup(n);
282                 
283                 /* Only include directory-part */
284                 p = strrchr(dn, '/');
285                 if(p) *p = '\0';
286
287                 /* Check if directory needs to be created */
288                 if(!g_file_test(dn, G_FILE_TEST_IS_DIR) && mkdir(dn, 0700) == -1) {
289                         log_network("log_custom", network, "Couldn't create directory %s for logging!", dn);
290                         g_free(dn);
291                         g_free(n);
292                         return NULL;
293                 }
294                 g_free(dn);
295                 
296                 /* Then open the correct filename */
297                 custom_subst(network, &n, logfilename, l, identifier, TRUE);
298                 f = fopen(n, "a+");
299                 if(!f) {
300                         log_network("log_custom", network, "Couldn't open file %s for logging!", n);
301                         g_free(n);
302                         return NULL;
303                 }
304                 g_hash_table_insert(files, n, f);
305                 g_free(n);
306         } else g_free(n);
307         return f;
308 }
309
310 static void file_write_target(struct network *network, const char *n, struct line *l) 
311 {
312         char *t, *s, *fmt;
313         FILE *f;
314
315         fmt = g_hash_table_lookup(fmts, n);
316         if(!fmt) return;
317
318         if(!irccmp(&network->state->info, network->state->me.nick, l->args[1])) {
319                 if(line_get_nick(l)) { t = g_strdup(line_get_nick(l)); }
320                 else { t = g_strdup("_messages_"); }
321         } else {
322                 t = g_strdup(l->args[1]);
323         }
324
325         f = find_add_channel_file(network, l, t);
326         if(!f) { g_free(t); return; }
327         
328         custom_subst(network, &s, fmt, l, t, FALSE);
329         g_free(t);
330
331     fputs(s, f);
332         fputc('\n', f);
333         fflush(f);
334
335         g_free(s);
336 }
337
338 static void file_write_channel_only(struct network *network, const char *n, struct line *l)
339 {
340         char *s, *fmt;
341         FILE *f;
342
343         fmt = g_hash_table_lookup(fmts, n);
344         if(!fmt) return;
345
346         f = find_add_channel_file(network, l, l->args[1]);
347         if(!f) return; 
348
349         custom_subst(network, &s, fmt, l, l->args[1], FALSE);
350
351         fputs(s, f); fputc('\n', f);
352         fflush(f);
353
354         g_free(s);
355 }
356
357 static void file_write_channel_query(struct network *network, const char *n, struct line *l)
358 {
359         char *s, *fmt;
360         char *nick;
361         FILE *f;
362         GList *gl;
363
364         nick = line_get_nick(l);
365         if(!nick)return;
366
367         fmt = g_hash_table_lookup(fmts, n);
368         if(!fmt) return;
369
370         /* check for the query first */
371         f = find_channel_file(network, l, nick);
372
373         if(f) {
374                 custom_subst(network, &s, fmt, l, nick, FALSE);
375                 fputs(s, f); fputc('\n', f);
376                 fflush(f);
377                 g_free(s);
378         }
379         
380         /* now, loop thru the channels and check if the user is there */
381         gl = network->state->channels;
382         while(gl) {
383                 struct channel_state *c = (struct channel_state *)gl->data;
384                 if(find_nick(c, nick)) {
385                         f = find_add_channel_file(network, l, c->name);
386                         if(f) {
387                                 custom_subst(network, &s, fmt, l, c->name, FALSE);
388                                 fputs(s, f); fputc('\n', f);
389                                 fflush(f);
390                                 g_free(s);
391                         }
392                 }
393                 gl = gl->next;
394         }
395 }
396
397 static gboolean log_custom_data(struct network *network, struct line *l, enum data_direction dir, void *userdata)
398 {
399         const char *nick = NULL;
400         char *user = NULL;
401         FILE *f = NULL;
402         if(!l->args || !l->args[0] || l->options & LINE_NO_LOGGING)return TRUE;
403         nick = line_get_nick(l);
404         if(user){ *user = '\0';user++; }
405
406         /* Loop thru possible values for %@ */
407
408         /* There are a few possibilities:
409          * - log to line_get_nick(l) or l->args[1] file depending on which 
410          *   was the current user (PRIVMSG, NOTICE, ACTION, MODE) (target)
411          * - log to all channels line_get_nick(l) was on, and to query, if applicable (NICK, QUIT) (channel_query)
412          * - log to channel only (KICK, PART, JOIN, TOPIC) (channel_only)
413          */
414
415         if(dir == FROM_SERVER && !g_strcasecmp(l->args[0], "JOIN")) {
416                 file_write_target(network, "join", l); 
417         } else if(dir == FROM_SERVER && !g_strcasecmp(l->args[0], "PART")) {
418                 file_write_channel_only(network, "part", l);
419         } else if(!g_strcasecmp(l->args[0], "PRIVMSG")) {
420                 if(l->args[2][0] == '\ 1') { 
421                         l->args[2][strlen(l->args[2])-1] = '\0';
422                         if(!g_ascii_strncasecmp(l->args[2], "\ 1ACTION ", 8)) { 
423                                 l->args[2]+=8;
424                                 file_write_target(network, "action", l);
425                                 l->args[2]-=8;
426                         }
427                         l->args[2][strlen(l->args[2])] = '\ 1';
428                         /* Ignore all other ctcp messages */
429                 } else {
430                         file_write_target(network, "msg", l);
431                 }
432         } else if(!g_strcasecmp(l->args[0], "NOTICE")) {
433                 file_write_target(network, "notice", l);
434         } else if(!g_strcasecmp(l->args[0], "MODE") && l->args[1] && 
435                           is_channelname(l->args[1], &network->state->info) && dir == FROM_SERVER) {
436                 file_write_target(network, "mode", l);
437         } else if(!g_strcasecmp(l->args[0], "QUIT")) {
438                 file_write_channel_query(network, "quit", l);
439         } else if(!g_strcasecmp(l->args[0], "KICK") && l->args[1] && l->args[2] && dir == FROM_SERVER) {
440                 if(!strchr(l->args[1], ',')) {
441                         file_write_channel_only(network, "kick", l);
442                 } else { 
443                         char *channels = g_strdup(l->args[1]);
444                         char *nicks = g_strdup(l->args[1]);
445                         char *p,*n; char cont = 1;
446                         char *_nick;
447
448                         p = channels;
449                         _nick = nicks;
450                         while(cont) {
451                                 n = strchr(p, ',');
452
453                                 if(!n) cont = 0;
454                                 else *n = '\0';
455
456                                 file_write_channel_only(network, "kick", l);
457
458                                 p = n+1;
459                                 _nick = strchr(_nick, ',');
460                                 if(!_nick)break;
461                                 _nick++;
462                         }
463                         
464                         g_free(channels);
465                         g_free(nicks);
466                 }
467         } else if(!g_strcasecmp(l->args[0], "TOPIC") && dir == FROM_SERVER && l->args[1]) {
468                 if(l->args[2]) file_write_channel_only(network, "topic", l);
469                 else file_write_channel_only(network, "notopic", l);
470         } else if(!g_strcasecmp(l->args[0], "NICK") && dir == FROM_SERVER && l->args[1]) {
471                 file_write_channel_query(network, "nickchange", l);
472         }
473
474         if(f) fflush(f);
475
476         return TRUE;
477 }
478
479 static gboolean fini_plugin(struct plugin *p)
480 {
481         del_log_filter("log_custom");
482         return TRUE;
483 }
484
485 static void savefmt(gpointer key, gpointer value, gpointer udata)
486 {
487         xmlNodePtr node = udata;
488
489         xmlNewTextChild(node, NULL, key, value);
490 }
491
492 static gboolean save_config(struct plugin *p, xmlNodePtr node)
493 {
494         xmlNewTextChild(node, NULL, "logfilename", logfilename);
495
496         g_hash_table_foreach(fmts, savefmt, node);
497
498         return TRUE;
499 }
500
501 static gboolean load_config(struct plugin *p, xmlNodePtr node)
502 {
503         xmlNodePtr cur;
504         
505         for (cur = node->children; cur; cur = cur->next) 
506         {
507                 if (cur->type != XML_ELEMENT_NODE) continue;
508
509                 if (!strcmp(cur->name, "logfilename")) {
510                         logfilename = xmlNodeGetContent(cur);
511                 } else {
512                         g_hash_table_insert(fmts, g_strdup(cur->name), xmlNodeGetContent(cur));
513                 }
514         }
515
516         return TRUE;
517 }
518
519 static gboolean init_plugin(struct plugin *p)
520 {
521         files = g_hash_table_new(g_str_hash, g_str_equal);
522         fmts = g_hash_table_new(g_str_hash, g_str_equal);
523         add_log_filter("log_custom", log_custom_data, NULL, 1000);
524         return TRUE;
525 }
526
527 struct plugin_ops plugin = {
528         .name = "log_custom",
529         .version = 0,
530         .init = init_plugin,
531         .fini = fini_plugin,
532         .load_config = load_config,
533         .save_config = save_config
534 };