Add 'say' socket command.
[vomak.git] / src / vomak.c
blobdab33c0f862fe96a6690212957bf868ef9a1a3c5
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 default: signame = "unknown"; break;
114 debug("Received %s, cleaning up", signame);
116 irc_goodbye(&irc_conn);
118 main_quit();
120 if (sig == SIGHUP)
121 irc_logfile_reopen(&irc_conn);
125 void main_quit()
127 #ifndef DEBUG
128 closelog();
129 #endif
130 irc_finalize(&irc_conn);
131 socket_finalize(&socket_info);
133 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 irc_conn.quit_msg = g_strdup(g_strchomp(buf + 5));
157 signal_cb(SIGTERM);
159 else if (strncmp("reload", buf, 6) == 0)
161 load_database();
163 else if (strncmp("openlog", buf, 7) == 0)
165 irc_logfile_reopen(&irc_conn);
167 else if (strncmp("say", buf, 3) == 0)
169 irc_send_message(&irc_conn, NULL, g_strchomp(buf + 4));
171 else if (strncmp("userlist", buf, 8) == 0)
173 if (userlist)
175 send(sock, userlist, strlen(userlist), 0);
176 send(sock, "\n", 1, 0);
178 else
179 send(sock, "error\n", 6, 0);
181 else
183 msg_len = g_snprintf(msg, sizeof msg, "%s\r\n", buf);
184 irc_send(&irc_conn, msg, msg_len);
185 vdebug(msg);
188 socket_fd_close(sock);
190 return TRUE;
194 static void hash_add_to_list(gpointer key, gpointer value, gpointer user_data)
196 if (key != NULL)
198 words = g_list_insert_sorted(words, key, (GCompareFunc) strcmp);
203 static gint write_file(const gchar *filename, const gchar *text)
205 FILE *fp;
206 gint bytes_written, len;
208 if (filename == NULL)
210 return ENOENT;
213 len = strlen(text);
215 fp = g_fopen(filename, "w");
216 if (fp != NULL)
218 bytes_written = fwrite(text, sizeof (gchar), len, fp);
219 fclose(fp);
221 if (len != bytes_written)
223 debug("written only %d bytes, had to write %d bytes to %s", bytes_written, len, filename);
224 return EIO;
227 else
229 debug("could not write to file %s (%s)", filename, g_strerror(errno));
230 return errno;
232 return 0;
236 void help_system_query(const gchar *msg)
238 gchar *result;
239 gchar *lowered_keyword;
241 if (strncmp("?? ", msg, 3) != 0)
242 return;
244 msg += 3;
246 lowered_keyword = g_utf8_strdown(msg, -1);
248 if (strncmp("keywords", lowered_keyword, 8) == 0)
250 GList *item;
251 GString *str = g_string_sized_new(256);
253 words = NULL;
255 g_hash_table_foreach(config->data, hash_add_to_list, NULL);
257 for (item = words; item != NULL; item = g_list_next(item))
259 g_string_append(str, item->data);
260 g_string_append_c(str, ' ');
263 irc_send_message(&irc_conn, NULL, "%s: %s", msg, str->str);
264 g_list_free(words);
265 g_string_free(str, TRUE);
267 else
269 result = g_hash_table_lookup(config->data, lowered_keyword);
272 * Look for the @-Char in the result string. If one is found, most
273 * likely the !alias-command was used and this has to be resolved
274 * recursively until no @ appears anymore in the string.
276 while (result && result[0] == '@')
277 result = g_hash_table_lookup(config->data, result + 1);
279 if (result)
280 irc_send_message(&irc_conn, NULL, "%s: %s", msg, result);
282 g_free(lowered_keyword);
286 // return value:
287 // 0 - added
288 // 1 - updated
289 // -1 - error
290 gint help_system_learn(const gchar *keyword, const gchar *text)
292 gchar *config_name;
293 gchar *data;
294 gchar *key;
295 gint ret = 0;
296 GKeyFile *keyfile;
298 TRACE
300 if (keyword == NULL || text == NULL)
301 return -1;
303 config_name = g_build_filename(getenv("HOME"), ".vomak", "database", NULL);
304 keyfile = g_key_file_new();
306 g_key_file_load_from_file(keyfile, config_name, G_KEY_FILE_NONE, NULL);
308 key = g_hash_table_lookup(config->data, keyword);
309 if (key != NULL)
310 ret = 1; // if key is non-NULL it is already available and gets updated
312 g_key_file_set_string(keyfile, CONFIG_SECTION_DATABASE, keyword, text);
314 data = g_key_file_to_data(keyfile, NULL, NULL);
315 write_file(config_name, data);
317 load_database_real(keyfile);
319 g_key_file_free(keyfile);
320 g_free(config_name);
321 g_free(data);
323 return ret;
327 static void config_free()
329 TRACE
330 g_free(config->socketname);
331 g_free(config->socketperm);
332 g_free(config->server);
333 g_free(config->channel);
334 g_free(config->servername);
335 g_free(config->nickname);
336 g_free(config->username);
337 g_free(config->realname);
338 g_free(config->nickserv_password);
339 g_free(config->logfile);
340 g_free(config->fortune_cmd);
341 if (config->data != NULL)
342 g_hash_table_destroy(config->data);
343 g_free(config);
344 config = NULL;
348 static config_t *config_read()
350 GKeyFile *keyfile = g_key_file_new();
351 gchar *config_name = g_build_filename(getenv("HOME"), ".vomak", "config", NULL);
352 config_t *config = g_new0(config_t, 1);
354 TRACE
356 g_key_file_load_from_file(keyfile, config_name, G_KEY_FILE_NONE, NULL);
358 config->socketname = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "socketname", NULL);
359 config->socketperm = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "socketperm", NULL);
360 config->logfile = g_key_file_get_string(keyfile, CONFIG_SECTION_GENERAL, "logfile", NULL);
361 config->query_names_interval = g_key_file_get_integer(keyfile, CONFIG_SECTION_GENERAL, "query_names_interval", NULL);
363 config->server = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "server", NULL);
364 config->port = g_key_file_get_integer(keyfile, CONFIG_SECTION_IRC, "port", NULL);
365 config->channel = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "channel", NULL);
366 config->servername = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "servername", NULL);
367 config->nickname = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "nickname", NULL);
368 config->username = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "username", NULL);
369 config->realname = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "realname", NULL);
370 config->nickserv_password = g_key_file_get_string(keyfile, CONFIG_SECTION_IRC, "nickserv_password", NULL);
371 config->auto_rejoin = g_key_file_get_boolean(keyfile, CONFIG_SECTION_IRC, "auto_rejoin", NULL);
373 config->fortune_cmd = g_key_file_get_string(keyfile, CONFIG_SECTION_EXTERNALS, "fortune_cmd", NULL);
375 g_key_file_free(keyfile);
376 g_free(config_name);
378 // if anything could not be read, abort. We don't use default values
379 if (config->socketname == NULL ||
380 config->socketperm == NULL ||
381 config->server == NULL ||
382 config->channel == NULL ||
383 config->servername == NULL ||
384 config->nickname == NULL ||
385 config->username == NULL ||
386 config->realname == NULL)
388 fprintf(stderr, "Config file does not exist or is not complete.\n");
389 exit(1);
392 return config;
396 static void init_socket()
398 TRACE
400 /* create unix domain socket for remote operation */
401 socket_info.lock_socket = -1;
402 socket_info.lock_socket_tag = 0;
403 socket_init(&socket_info, config->socketname);
405 if (socket_info.lock_socket >= 0)
407 mode_t mode;
408 sscanf(config->socketperm, "%o", &mode);
409 /// FIXME for some reason the sticky bit is set when socketperm is "0640"
410 chmod(config->socketname, mode);
411 socket_info.read_ioc = g_io_channel_unix_new(socket_info.lock_socket);
412 g_io_channel_set_flags(socket_info.read_ioc, G_IO_FLAG_NONBLOCK, NULL);
413 socket_info.lock_socket_tag = g_io_add_watch(socket_info.read_ioc,
414 G_IO_IN|G_IO_PRI|G_IO_ERR, socket_input_cb, NULL);
419 void set_user_list(const gchar *list)
421 g_free(userlist);
422 userlist = g_strdup(list);
426 const gchar *get_user_list()
428 return userlist;
432 gint main(gint argc, gchar **argv)
434 irc_conn.quit_msg = NULL;
436 // init stuff
437 main_loop = g_main_loop_new(NULL, FALSE);
438 config = config_read();
439 load_database();
440 init_socket();
441 irc_connect(&irc_conn);
442 signal(SIGTERM, signal_cb);
443 signal(SIGINT, signal_cb);
445 if (config->query_names_interval)
446 g_timeout_add(config->query_names_interval * 60000, irc_query_names, &irc_conn);
448 // if not a debug build, deattach from console and go into background
449 #ifndef DEBUG
450 signal(SIGTTOU, SIG_IGN);
451 signal(SIGTTIN, SIG_IGN);
452 signal(SIGTSTP, SIG_IGN);
453 openlog("vomak", LOG_PID, LOG_DAEMON);
455 if (daemon(1, 0) < 0)
457 g_printerr("Unable to daemonize\n");
458 return -1;
460 #endif
462 // main loop
463 g_main_loop_run(main_loop);
465 config_free();
467 return 0;
471 /* if debug mode is enabled, print the warning to stderr, else log it with syslog */
472 void vomak_warning(gchar const *format, ...)
474 va_list args;
475 va_start(args, format);
477 #ifdef DEBUG
478 g_logv(G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, format, args);
479 #else
480 vsyslog(LOG_WARNING, format, args);
481 #endif
483 va_end(args);