Cleanly quit if we got an error
[vomak.git] / src / vomak.c
blobc8fff4798cc09152814b8476d73ac94c5cafe9db
1 /*
2 * vomak.c - this file is part of vomak - a very simple IRC bot
4 * Copyright 2008 Enrico Tröger <enrico(dot)troeger(at)uvena(dot)de>
5 * Copyright 2008 Dominic Hopf <dh(at)dmaphy(dot)de>
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; version 2 of the License.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23 #define _GNU_SOURCE
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <errno.h>
28 #include <netdb.h>
29 #include <sys/types.h>
30 #include <sys/stat.h>
31 #include <netinet/in.h>
32 #include <sys/socket.h>
33 #include <unistd.h>
34 #include <string.h>
35 #include <signal.h>
36 #ifndef DEBUG
37 # include <syslog.h>
38 # include <stdarg.h>
39 #endif
41 #include <glib.h>
42 #include <glib/gstdio.h>
44 #include "vomak.h"
45 #include "socket.h"
46 #include "irc.h"
48 #define CONFIG_SECTION_GENERAL "general"
49 #define CONFIG_SECTION_IRC "irc"
50 #define CONFIG_SECTION_EXTERNALS "externals"
51 #define CONFIG_SECTION_DATABASE "general"
54 static irc_conn_t irc_conn;
55 static socket_info_t socket_info;
56 static GMainLoop *main_loop = NULL;
57 static gchar *userlist = NULL;
58 static GList *words = NULL;
59 config_t *config;
61 static void load_database_real(GKeyFile *keyfile)
63 gsize j, len_keys = 0;
64 gchar **keys;
66 TRACE
68 // unload previously loaded data, aka reload
69 if (config->data != NULL)
71 g_hash_table_destroy(config->data);
74 config->data = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
76 keys = g_key_file_get_keys(keyfile, CONFIG_SECTION_DATABASE, &len_keys, NULL);
77 for (j = 0; j < len_keys; j++)
79 g_hash_table_insert(config->data, g_utf8_strdown(keys[j], -1),
80 g_key_file_get_string(keyfile, CONFIG_SECTION_DATABASE, keys[j], NULL));
82 g_strfreev(keys);
86 static void load_database()
88 gchar *config_name = g_build_filename(getenv("HOME"), ".vomak", "database", NULL);
89 GKeyFile *keyfile = g_key_file_new();
91 TRACE
93 g_key_file_load_from_file(keyfile, config_name, G_KEY_FILE_NONE, NULL);
95 load_database_real(keyfile);
97 g_key_file_free(keyfile);
98 g_free(config_name);
102 static void signal_cb(gint sig)
104 if (sig == SIGTERM || sig == SIGINT)
106 gchar *signame;
108 switch (sig)
110 case SIGTERM: signame = "SIGTERM"; break;
111 case SIGINT: signame = "SIGINT"; break;
112 case SIGPIPE: signame = "SIGPIPE"; break;
113 default: signame = "unknown"; break;
115 debug("Received %s, cleaning up", signame);
117 irc_goodbye(&irc_conn);
119 main_quit();
121 if (sig == SIGHUP)
122 irc_logfile_reopen(&irc_conn);
126 void main_quit()
128 #ifndef DEBUG
129 closelog();
130 #endif
131 irc_finalize(&irc_conn);
132 socket_finalize(&socket_info);
134 g_main_loop_quit(main_loop);
139 static gboolean socket_input_cb(GIOChannel *ioc, GIOCondition cond, gpointer data)
141 gint fd, sock;
142 static gchar buf[1024];
143 static gchar msg[1024];
144 struct sockaddr_in caddr;
145 guint caddr_len, len, msg_len;
147 caddr_len = sizeof(caddr);
149 fd = g_io_channel_unix_get_fd(ioc);
150 sock = accept(fd, (struct sockaddr *)&caddr, &caddr_len);
152 TRACE
153 while ((len = socket_fd_gets(sock, buf, sizeof(buf))) != -1)
155 if (strncmp("quit", buf, 4) == 0)
157 g_free(irc_conn.quit_msg);
158 irc_conn.quit_msg = g_strdup(g_strchomp(buf + 5));
159 signal_cb(SIGTERM);
161 else if (strncmp("reload", buf, 6) == 0)
163 load_database();
165 else if (strncmp("openlog", buf, 7) == 0)
167 irc_logfile_reopen(&irc_conn);
169 else if (strncmp("say", buf, 3) == 0)
171 irc_send_message(&irc_conn, NULL, g_strchomp(buf + 4));
173 else if (strncmp("userlist", buf, 8) == 0)
175 if (userlist)
177 send(sock, userlist, strlen(userlist), MSG_NOSIGNAL);
178 send(sock, "\n", 1, MSG_NOSIGNAL);
180 else
181 send(sock, "error\n", 6, MSG_NOSIGNAL);
183 else
185 msg_len = g_snprintf(msg, sizeof msg, "%s\r\n", buf);
186 irc_send(&irc_conn, msg, msg_len);
187 vdebug(msg);
190 socket_fd_close(sock);
192 return TRUE;
196 static void hash_add_to_list(gpointer key, gpointer value, gpointer user_data)
198 if (key != NULL)
200 words = g_list_insert_sorted(words, key, (GCompareFunc) strcmp);
205 static gint write_file(const gchar *filename, const gchar *text)
207 FILE *fp;
208 gint bytes_written, len;
210 if (filename == NULL)
212 return ENOENT;
215 len = strlen(text);
217 fp = g_fopen(filename, "w");
218 if (fp != NULL)
220 bytes_written = fwrite(text, sizeof (gchar), len, fp);
221 fclose(fp);
223 if (len != bytes_written)
225 debug("written only %d bytes, had to write %d bytes to %s", bytes_written, len, filename);
226 return EIO;
229 else
231 debug("could not write to file %s (%s)", filename, g_strerror(errno));
232 return errno;
234 return 0;
238 void help_system_query(const gchar *msg)
240 gchar *result;
241 gchar *lowered_keyword;
243 if (strncmp("?? ", msg, 3) != 0)
244 return;
246 msg += 3;
248 lowered_keyword = g_utf8_strdown(msg, -1);
250 if (strncmp("keywords", lowered_keyword, 8) == 0)
252 GList *item;
253 GString *str = g_string_sized_new(256);
255 words = NULL;
257 g_hash_table_foreach(config->data, hash_add_to_list, NULL);
259 for (item = words; item != NULL; item = g_list_next(item))
261 g_string_append(str, item->data);
262 g_string_append_c(str, ' ');
265 irc_send_message(&irc_conn, NULL, "%s: %s", msg, str->str);
266 g_list_free(words);
267 g_string_free(str, TRUE);
269 else
271 result = g_hash_table_lookup(config->data, lowered_keyword);
274 * Look for the @-Char in the result string. If one is found, most
275 * likely the !alias-command was used and this has to be resolved
276 * recursively until no @ appears anymore in the string.
278 while (result && result[0] == '@')
279 result = g_hash_table_lookup(config->data, result + 1);
281 if (result)
282 irc_send_message(&irc_conn, NULL, "%s: %s", msg, result);
284 g_free(lowered_keyword);
288 // return value:
289 // 0 - added
290 // 1 - updated
291 // -1 - error
292 gint help_system_learn(const gchar *keyword, const gchar *text)
294 gchar *config_name;
295 gchar *data;
296 gchar *key;
297 gint ret = 0;
298 GKeyFile *keyfile;
300 TRACE
302 if (keyword == NULL || text == NULL)
303 return -1;
305 config_name = g_build_filename(getenv("HOME"), ".vomak", "database", NULL);
306 keyfile = g_key_file_new();
308 g_key_file_load_from_file(keyfile, config_name, G_KEY_FILE_NONE, NULL);
310 key = g_hash_table_lookup(config->data, keyword);
311 if (key != NULL)
312 ret = 1; // if key is non-NULL it is already available and gets updated
314 g_key_file_set_string(keyfile, CONFIG_SECTION_DATABASE, keyword, text);
316 data = g_key_file_to_data(keyfile, NULL, NULL);
317 write_file(config_name, data);
319 load_database_real(keyfile);
321 g_key_file_free(keyfile);
322 g_free(config_name);
323 g_free(data);
325 return ret;
329 static void config_free()
331 TRACE
332 g_free(config->socketname);
333 g_free(config->socketperm);
334 g_free(config->server);
335 g_free(config->channel);
336 g_free(config->servername);
337 g_free(config->nickname);
338 g_free(config->username);
339 g_free(config->realname);
340 g_free(config->nickserv_password);
341 g_free(config->logfile);
342 g_free(config->fortune_cmd);
343 if (config->data != NULL)
344 g_hash_table_destroy(config->data);
345 g_free(config);
346 config = NULL;
350 static config_t *config_read()
352 GKeyFile *keyfile = g_key_file_new();
353 gchar *config_name = g_build_filename(getenv("HOME"), ".vomak", "config", NULL);
354 config_t *config = g_new0(config_t, 1);
356 TRACE
358 g_key_file_load_from_file(keyfile, config_name, G_KEY_FILE_NONE, NULL);
360 config->socketname = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "socketname", NULL);
361 config->socketperm = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "socketperm", NULL);
362 config->logfile = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "logfile", NULL);
363 config->query_names_interval = g_key_file_get_integer(keyfile, CONFIG_SECTION_GENERAL, "query_names_interval", NULL);
365 config->server = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "server", NULL);
366 config->port = g_key_file_get_integer(keyfile, CONFIG_SECTION_IRC, "port", NULL);
367 config->channel = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "channel", NULL);
368 config->servername = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "servername", NULL);
369 config->nickname = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "nickname", NULL);
370 config->username = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "username", NULL);
371 config->realname = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "realname", NULL);
372 config->nickserv_password = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "nickserv_password", NULL);
373 config->auto_rejoin = g_key_file_get_boolean(keyfile, CONFIG_SECTION_IRC, "auto_rejoin", NULL);
375 config->fortune_cmd = g_key_file_get_string(keyfile, CONFIG_SECTION_EXTERNALS, "fortune_cmd", NULL);
377 g_key_file_free(keyfile);
378 g_free(config_name);
380 // if anything could not be read, abort. We don't use default values
381 if (config->socketname == NULL ||
382 config->socketperm == NULL ||
383 config->server == NULL ||
384 config->channel == NULL ||
385 config->servername == NULL ||
386 config->nickname == NULL ||
387 config->username == NULL ||
388 config->realname == NULL)
390 fprintf(stderr, "Config file does not exist or is not complete.\n");
391 exit(1);
394 return config;
398 static void init_socket()
400 TRACE
402 /* create unix domain socket for remote operation */
403 socket_info.lock_socket = -1;
404 socket_info.lock_socket_tag = 0;
405 socket_init(&socket_info, config->socketname);
407 if (socket_info.lock_socket >= 0)
409 mode_t mode;
410 sscanf(config->socketperm, "%o", &mode);
411 /// FIXME for some reason the sticky bit is set when socketperm is "0640"
412 chmod(config->socketname, mode);
413 socket_info.read_ioc = g_io_channel_unix_new(socket_info.lock_socket);
414 g_io_channel_set_flags(socket_info.read_ioc, G_IO_FLAG_NONBLOCK, NULL);
415 socket_info.lock_socket_tag = g_io_add_watch(socket_info.read_ioc,
416 G_IO_IN|G_IO_PRI|G_IO_ERR, socket_input_cb, NULL);
421 void set_user_list(const gchar *list)
423 g_free(userlist);
424 userlist = g_strdup(list);
428 const gchar *get_user_list()
430 return userlist;
434 gint main(gint argc, gchar **argv)
436 irc_conn.quit_msg = NULL;
438 // init stuff
439 main_loop = g_main_loop_new(NULL, FALSE);
440 config = config_read();
441 load_database();
442 init_socket();
443 irc_connect(&irc_conn);
444 signal(SIGTERM, signal_cb);
445 signal(SIGINT, signal_cb);
447 if (config->query_names_interval)
448 g_timeout_add(config->query_names_interval * 60000, irc_query_names, &irc_conn);
450 // if not a debug build, deattach from console and go into background
451 #ifndef DEBUG
452 signal(SIGTTOU, SIG_IGN);
453 signal(SIGTTIN, SIG_IGN);
454 signal(SIGTSTP, SIG_IGN);
455 openlog("vomak", LOG_PID, LOG_DAEMON);
457 if (daemon(1, 0) < 0)
459 g_printerr("Unable to daemonize\n");
460 return -1;
462 #endif
464 // main loop
465 g_main_loop_run(main_loop);
467 config_free();
469 return 0;
473 /* if debug mode is enabled, print the warning to stderr, else log it with syslog */
474 void vomak_warning(gchar const *format, ...)
476 va_list args;
477 va_start(args, format);
479 #ifdef DEBUG
480 g_logv(G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, format, args);
481 #else
482 vsyslog(LOG_WARNING, format, args);
483 #endif
485 va_end(args);