You know the game, fix URL, EOL.
[vomak.git] / src / vomak.c
blob8cd3a437d8506eeb433080f13cb2c9170d26e4bd
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);
138 static gboolean socket_input_cb(GIOChannel *ioc, GIOCondition cond, gpointer data)
140 gint fd, sock;
141 static gchar buf[1024];
142 static gchar msg[1024];
143 struct sockaddr_in caddr;
144 guint caddr_len, len, msg_len;
146 caddr_len = sizeof(caddr);
148 fd = g_io_channel_unix_get_fd(ioc);
149 sock = accept(fd, (struct sockaddr *)&caddr, &caddr_len);
151 TRACE
152 while ((len = socket_fd_gets(sock, buf, sizeof(buf))) != -1)
154 if (strncmp("quit", buf, 4) == 0)
156 g_free(irc_conn.quit_msg);
157 irc_conn.quit_msg = g_strdup(g_strchomp(buf + 5));
158 signal_cb(SIGTERM);
160 else if (strncmp("reload", buf, 6) == 0)
162 load_database();
164 else if (strncmp("openlog", buf, 7) == 0)
166 irc_logfile_reopen(&irc_conn);
168 else if (strncmp("say", buf, 3) == 0)
170 irc_send_message(&irc_conn, NULL, g_strchomp(buf + 4));
172 else if (strncmp("userlist", buf, 8) == 0)
174 if (userlist)
176 send(sock, userlist, strlen(userlist), MSG_NOSIGNAL);
177 send(sock, "\n", 1, MSG_NOSIGNAL);
179 else
180 send(sock, "error\n", 6, MSG_NOSIGNAL);
182 else
184 msg_len = g_snprintf(msg, sizeof msg, "%s\r\n", buf);
185 irc_send(&irc_conn, msg, msg_len);
186 vdebug(msg);
189 socket_fd_close(sock);
191 return TRUE;
195 static void hash_add_to_list(gpointer key, gpointer value, gpointer user_data)
197 if (key != NULL)
199 words = g_list_insert_sorted(words, key, (GCompareFunc) strcmp);
204 static gint write_file(const gchar *filename, const gchar *text)
206 FILE *fp;
207 gint bytes_written, len;
209 if (filename == NULL)
211 return ENOENT;
214 len = strlen(text);
216 fp = g_fopen(filename, "w");
217 if (fp != NULL)
219 bytes_written = fwrite(text, sizeof (gchar), len, fp);
220 fclose(fp);
222 if (len != bytes_written)
224 debug("written only %d bytes, had to write %d bytes to %s", bytes_written, len, filename);
225 return EIO;
228 else
230 debug("could not write to file %s (%s)", filename, g_strerror(errno));
231 return errno;
233 return 0;
237 void help_system_query(const gchar *msg)
239 gchar *result;
240 gchar *lowered_keyword;
242 if (strncmp("?? ", msg, 3) != 0)
243 return;
245 msg += 3;
247 lowered_keyword = g_utf8_strdown(msg, -1);
249 if (strncmp("keywords", lowered_keyword, 8) == 0)
251 GList *item;
252 GString *str = g_string_sized_new(256);
254 words = NULL;
256 g_hash_table_foreach(config->data, hash_add_to_list, NULL);
258 for (item = words; item != NULL; item = g_list_next(item))
260 g_string_append(str, item->data);
261 g_string_append_c(str, ' ');
264 irc_send_message(&irc_conn, NULL, "%s: %s", msg, str->str);
265 g_list_free(words);
266 g_string_free(str, TRUE);
268 else
270 result = g_hash_table_lookup(config->data, lowered_keyword);
273 * Look for the @-Char in the result string. If one is found, most
274 * likely the !alias-command was used and this has to be resolved
275 * recursively until no @ appears anymore in the string.
277 while (result && result[0] == '@')
278 result = g_hash_table_lookup(config->data, result + 1);
280 if (result)
281 irc_send_message(&irc_conn, NULL, "%s: %s", msg, result);
283 g_free(lowered_keyword);
287 // return value:
288 // 0 - added
289 // 1 - updated
290 // -1 - error
291 gint help_system_learn(const gchar *keyword, const gchar *text)
293 gchar *config_name;
294 gchar *data;
295 gchar *key;
296 gint ret = 0;
297 GKeyFile *keyfile;
299 TRACE
301 if (keyword == NULL || text == NULL)
302 return -1;
304 config_name = g_build_filename(getenv("HOME"), ".vomak", "database", NULL);
305 keyfile = g_key_file_new();
307 g_key_file_load_from_file(keyfile, config_name, G_KEY_FILE_NONE, NULL);
309 key = g_hash_table_lookup(config->data, keyword);
310 if (key != NULL)
311 ret = 1; // if key is non-NULL it is already available and gets updated
313 g_key_file_set_string(keyfile, CONFIG_SECTION_DATABASE, keyword, text);
315 data = g_key_file_to_data(keyfile, NULL, NULL);
316 write_file(config_name, data);
318 load_database_real(keyfile);
320 g_key_file_free(keyfile);
321 g_free(config_name);
322 g_free(data);
324 return ret;
328 static void config_free()
330 TRACE
331 g_free(config->socketname);
332 g_free(config->socketperm);
333 g_free(config->server);
334 g_free(config->channel);
335 g_free(config->servername);
336 g_free(config->nickname);
337 g_free(config->username);
338 g_free(config->realname);
339 g_free(config->nickserv_password);
340 g_free(config->logfile);
341 g_free(config->fortune_cmd);
342 if (config->data != NULL)
343 g_hash_table_destroy(config->data);
344 g_free(config);
345 config = NULL;
349 static config_t *config_read()
351 GKeyFile *keyfile = g_key_file_new();
352 gchar *config_name = g_build_filename(getenv("HOME"), ".vomak", "config", NULL);
353 config_t *config = g_new0(config_t, 1);
355 TRACE
357 g_key_file_load_from_file(keyfile, config_name, G_KEY_FILE_NONE, NULL);
359 config->socketname = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "socketname", NULL);
360 config->socketperm = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "socketperm", NULL);
361 config->logfile = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "logfile", NULL);
362 config->query_names_interval = g_key_file_get_integer(keyfile, CONFIG_SECTION_GENERAL, "query_names_interval", NULL);
364 config->server = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "server", NULL);
365 config->port = g_key_file_get_integer(keyfile, CONFIG_SECTION_IRC, "port", NULL);
366 config->channel = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "channel", NULL);
367 config->servername = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "servername", NULL);
368 config->nickname = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "nickname", NULL);
369 config->username = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "username", NULL);
370 config->realname = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "realname", NULL);
371 config->nickserv_password = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "nickserv_password", NULL);
372 config->auto_rejoin = g_key_file_get_boolean(keyfile, CONFIG_SECTION_IRC, "auto_rejoin", NULL);
374 config->fortune_cmd = g_key_file_get_string(keyfile, CONFIG_SECTION_EXTERNALS, "fortune_cmd", NULL);
376 g_key_file_free(keyfile);
377 g_free(config_name);
379 // if anything could not be read, abort. We don't use default values
380 if (config->socketname == NULL ||
381 config->socketperm == NULL ||
382 config->server == NULL ||
383 config->channel == NULL ||
384 config->servername == NULL ||
385 config->nickname == NULL ||
386 config->username == NULL ||
387 config->realname == NULL)
389 fprintf(stderr, "Config file does not exist or is not complete.\n");
390 exit(1);
393 return config;
397 static void init_socket()
399 TRACE
401 /* create unix domain socket for remote operation */
402 socket_info.lock_socket = -1;
403 socket_info.lock_socket_tag = 0;
404 socket_init(&socket_info, config->socketname);
406 if (socket_info.lock_socket >= 0)
408 mode_t mode;
409 sscanf(config->socketperm, "%o", &mode);
410 /// FIXME for some reason the sticky bit is set when socketperm is "0640"
411 chmod(config->socketname, mode);
412 socket_info.read_ioc = g_io_channel_unix_new(socket_info.lock_socket);
413 g_io_channel_set_flags(socket_info.read_ioc, G_IO_FLAG_NONBLOCK, NULL);
414 socket_info.lock_socket_tag = g_io_add_watch(socket_info.read_ioc,
415 G_IO_IN|G_IO_PRI|G_IO_ERR, socket_input_cb, NULL);
420 void set_user_list(const gchar *list)
422 g_free(userlist);
423 userlist = g_strdup(list);
427 const gchar *get_user_list()
429 return userlist;
433 gint main(gint argc, gchar **argv)
435 irc_conn.quit_msg = NULL;
437 // init stuff
438 main_loop = g_main_loop_new(NULL, FALSE);
439 config = config_read();
440 load_database();
441 init_socket();
442 irc_connect(&irc_conn);
443 signal(SIGTERM, signal_cb);
444 signal(SIGINT, signal_cb);
446 if (config->query_names_interval)
447 g_timeout_add(config->query_names_interval * 60000, irc_query_names, &irc_conn);
449 // if not a debug build, deattach from console and go into background
450 #ifndef DEBUG
451 signal(SIGTTOU, SIG_IGN);
452 signal(SIGTTIN, SIG_IGN);
453 signal(SIGTSTP, SIG_IGN);
454 openlog("vomak", LOG_PID, LOG_DAEMON);
456 if (daemon(1, 0) < 0)
458 g_printerr("Unable to daemonize\n");
459 return -1;
461 #endif
463 // main loop
464 g_main_loop_run(main_loop);
466 config_free();
468 return 0;
472 /* if debug mode is enabled, print the warning to stderr, else log it with syslog */
473 void vomak_warning(gchar const *format, ...)
475 va_list args;
476 va_start(args, format);
478 #ifdef DEBUG
479 g_logv(G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, format, args);
480 #else
481 vsyslog(LOG_WARNING, format, args);
482 #endif
484 va_end(args);