Implement !fortune command.
[vomak.git] / src / vomak.c
blobfc2c37d45f30f194e9e2c8a1abf137e9ffe7ce77
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 #endif
40 #include <glib.h>
41 #include <glib/gstdio.h>
43 #include "vomak.h"
44 #include "socket.h"
45 #include "irc.h"
47 #define CONFIG_SECTION_GENERAL "general"
48 #define CONFIG_SECTION_DATABASE "general"
51 static irc_conn_t irc_conn;
52 static socket_info_t socket_info;
53 static GMainLoop *main_loop = NULL;
54 static gchar *userlist = NULL;
55 static GList *words = NULL;
56 config_t *config;
58 static void load_database_real(GKeyFile *keyfile)
60 gsize j, len_keys = 0;
61 gchar **keys;
63 TRACE
65 // unload previously loaded data, aka reload
66 if (config->data != NULL)
68 g_hash_table_destroy(config->data);
71 config->data = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
73 keys = g_key_file_get_keys(keyfile, CONFIG_SECTION_DATABASE, &len_keys, NULL);
74 for (j = 0; j < len_keys; j++)
76 g_hash_table_insert(config->data, g_utf8_strdown(keys[j], -1),
77 g_key_file_get_string(keyfile, CONFIG_SECTION_DATABASE, keys[j], NULL));
79 g_strfreev(keys);
83 static void load_database()
85 gchar *config_name = g_build_filename(getenv("HOME"), ".vomak", "database", NULL);
86 GKeyFile *keyfile = g_key_file_new();
88 TRACE
90 g_key_file_load_from_file(keyfile, config_name, G_KEY_FILE_NONE, NULL);
92 load_database_real(keyfile);
94 g_key_file_free(keyfile);
95 g_free(config_name);
99 static void signal_cb(gint sig)
101 if (sig == SIGTERM || sig == SIGINT)
103 gchar *signame;
105 switch (sig)
107 case SIGTERM: signame = "SIGTERM"; break;
108 case SIGINT: signame = "SIGINT"; break;
109 default: signame = "unknown"; break;
111 debug("Received %s, cleaning up", signame);
113 irc_goodbye(&irc_conn);
115 main_quit();
117 if (sig == SIGHUP)
118 irc_logfile_reopen(&irc_conn);
122 void main_quit()
124 #ifndef DEBUG
125 closelog();
126 #endif
127 irc_finalize(&irc_conn);
128 socket_finalize(&socket_info);
130 g_main_loop_quit(main_loop);
135 static gboolean socket_input_cb(GIOChannel *ioc, GIOCondition cond, gpointer data)
137 gint fd, sock;
138 static gchar buf[1024];
139 static gchar msg[1024];
140 struct sockaddr_in caddr;
141 guint caddr_len, len, msg_len;
143 caddr_len = sizeof(caddr);
145 fd = g_io_channel_unix_get_fd(ioc);
146 sock = accept(fd, (struct sockaddr *)&caddr, &caddr_len);
148 TRACE
149 while ((len = socket_fd_gets(sock, buf, sizeof(buf))) != -1)
151 if (strncmp("quit", buf, 4) == 0)
153 signal_cb(SIGTERM);
155 else if (strncmp("reload", buf, 6) == 0)
157 load_database();
159 else if (strncmp("openlog", buf, 7) == 0)
161 irc_logfile_reopen(&irc_conn);
163 else if (strncmp("userlist", buf, 8) == 0)
165 if (userlist)
167 send(sock, userlist, strlen(userlist), 0);
168 send(sock, "\n", 1, 0);
170 else
171 send(sock, "error\n", 6, 0);
173 else
175 msg_len = g_snprintf(msg, sizeof msg, "%s\r\n", buf);
176 irc_send(&irc_conn, msg, msg_len);
177 vdebug(msg);
180 socket_fd_close(sock);
182 return TRUE;
186 static void hash_add_to_list(gpointer key, gpointer value, gpointer user_data)
188 if (key != NULL)
190 words = g_list_insert_sorted(words, key, (GCompareFunc) strcmp);
195 static gint write_file(const gchar *filename, const gchar *text)
197 FILE *fp;
198 gint bytes_written, len;
200 if (filename == NULL)
202 return ENOENT;
205 len = strlen(text);
207 fp = g_fopen(filename, "w");
208 if (fp != NULL)
210 bytes_written = fwrite(text, sizeof (gchar), len, fp);
211 fclose(fp);
213 if (len != bytes_written)
215 debug("written only %d bytes, had to write %d bytes to %s", bytes_written, len, filename);
216 return EIO;
219 else
221 debug("could not write to file %s (%s)", filename, g_strerror(errno));
222 return errno;
224 return 0;
228 void help_system_query(const gchar *msg)
230 gchar *result;
231 gchar *lowered_keyword;
233 if (strncmp("?? ", msg, 3) != 0)
234 return;
236 msg += 3;
238 lowered_keyword = g_utf8_strdown(msg, -1);
240 if (strncmp("keywords", lowered_keyword, 8) == 0)
242 GList *item;
243 GString *str = g_string_sized_new(256);
245 words = NULL;
247 g_hash_table_foreach(config->data, hash_add_to_list, NULL);
249 for (item = words; item != NULL; item = g_list_next(item))
251 g_string_append(str, item->data);
252 g_string_append_c(str, ' ');
255 irc_send_message(&irc_conn, NULL, "%s: %s", msg, str->str);
256 g_list_free(words);
257 g_string_free(str, TRUE);
259 else
261 result = g_hash_table_lookup(config->data, lowered_keyword);
264 * Look for the @-Char in the result string. If one is found, most
265 * likely the !alias-command was used and this has to be resolved
266 * recursively until no @ appears anymore in the string.
268 while (result && result[0] == '@')
269 result = g_hash_table_lookup(config->data, result + 1);
271 if (result)
272 irc_send_message(&irc_conn, NULL, "%s: %s", msg, result);
274 g_free(lowered_keyword);
278 // return value:
279 // 0 - added
280 // 1 - updated
281 // -1 - error
282 gint help_system_learn(const gchar *keyword, const gchar *text)
284 gchar *config_name;
285 gchar *data;
286 gchar *key;
287 gint ret = 0;
288 GKeyFile *keyfile;
290 TRACE
292 if (keyword == NULL || text == NULL)
293 return -1;
295 config_name = g_build_filename(getenv("HOME"), ".vomak", "database", NULL);
296 keyfile = g_key_file_new();
298 g_key_file_load_from_file(keyfile, config_name, G_KEY_FILE_NONE, NULL);
300 key = g_hash_table_lookup(config->data, keyword);
301 if (key != NULL)
302 ret = 1; // if key is non-NULL it is already available and gets updated
304 g_key_file_set_string(keyfile, CONFIG_SECTION_DATABASE, keyword, text);
306 data = g_key_file_to_data(keyfile, NULL, NULL);
307 write_file(config_name, data);
309 load_database_real(keyfile);
311 g_key_file_free(keyfile);
312 g_free(config_name);
313 g_free(data);
315 return ret;
319 static void config_free()
321 TRACE
322 g_free(config->socketname);
323 g_free(config->socketperm);
324 g_free(config->server);
325 g_free(config->channel);
326 g_free(config->servername);
327 g_free(config->nickname);
328 g_free(config->username);
329 g_free(config->realname);
330 g_free(config->nickserv_password);
331 g_free(config->logfile);
332 g_free(config->fortune_cmd);
333 if (config->data != NULL)
334 g_hash_table_destroy(config->data);
335 g_free(config);
336 config = NULL;
340 static config_t *config_read()
342 GKeyFile *keyfile = g_key_file_new();
343 gchar *config_name = g_build_filename(getenv("HOME"), ".vomak", "config", NULL);
344 config_t *config = g_new0(config_t, 1);
346 TRACE
348 g_key_file_load_from_file(keyfile, config_name, G_KEY_FILE_NONE, NULL);
350 config->socketname = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "socketname", NULL);
351 config->socketperm = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "socketperm", NULL);
352 config->server = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "server", NULL);
353 config->port = g_key_file_get_integer(keyfile, CONFIG_SECTION_GENERAL, "port", NULL);
354 config->channel = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "channel", NULL);
355 config->servername = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "servername", NULL);
356 config->nickname = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "nickname", NULL);
357 config->username = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "username", NULL);
358 config->realname = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "realname", NULL);
359 config->nickserv_password = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "nickserv_password", NULL);
360 config->logfile = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "logfile", NULL);
361 config->fortune_cmd = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "fortune_cmd", NULL);
363 g_key_file_free(keyfile);
364 g_free(config_name);
366 // if anything could not be read, abort. We don't use default values
367 if (config->socketname == NULL ||
368 config->socketperm == NULL ||
369 config->server == NULL ||
370 config->channel == NULL ||
371 config->servername == NULL ||
372 config->nickname == NULL ||
373 config->username == NULL ||
374 config->realname == NULL)
376 fprintf(stderr, "Config file does not exist or is not complete.\n");
377 exit(1);
380 return config;
384 static void init_socket()
386 TRACE
388 /* create unix domain socket for remote operation */
389 socket_info.lock_socket = -1;
390 socket_info.lock_socket_tag = 0;
391 socket_init(&socket_info, config->socketname);
393 if (socket_info.lock_socket >= 0)
395 mode_t mode;
396 sscanf(config->socketperm, "%o", &mode);
397 /// FIXME for some reason the sticky bit is set when socketperm is "0640"
398 chmod(config->socketname, mode);
399 socket_info.read_ioc = g_io_channel_unix_new(socket_info.lock_socket);
400 g_io_channel_set_flags(socket_info.read_ioc, G_IO_FLAG_NONBLOCK, NULL);
401 socket_info.lock_socket_tag = g_io_add_watch(socket_info.read_ioc,
402 G_IO_IN|G_IO_PRI|G_IO_ERR, socket_input_cb, NULL);
407 static gboolean mark_connect(gpointer data)
409 TRACE
411 // delay the internal connected status to skip parsing of MOTD messages
412 irc_conn.connected = TRUE;
414 return FALSE;
418 void set_user_list(const gchar *list)
420 g_free(userlist);
421 userlist = g_strdup(list);
425 const gchar *get_user_list()
427 return userlist;
431 gint main(gint argc, gchar **argv)
433 irc_conn.connected = FALSE;
435 // init stuff
436 main_loop = g_main_loop_new(NULL, FALSE);
437 config = config_read();
438 load_database();
439 init_socket();
440 irc_connect(&irc_conn);
441 signal(SIGTERM, signal_cb);
442 signal(SIGINT, signal_cb);
444 g_timeout_add(15000, mark_connect, NULL);
445 g_timeout_add(180000, irc_query_names, &irc_conn);
447 // if not a debug build, deattach from console and go into background
448 #ifndef DEBUG
449 signal(SIGTTOU, SIG_IGN);
450 signal(SIGTTIN, SIG_IGN);
451 signal(SIGTSTP, SIG_IGN);
452 openlog("vomak", LOG_PID, LOG_DAEMON);
454 if (daemon(1, 0) < 0)
456 g_printerr("Unable to daemonize\n");
457 return -1;
459 #endif
461 // main loop
462 g_main_loop_run(main_loop);
464 config_free();
466 return 0;