/* * degesch.c: the experimental IRC client * * Copyright (c) 2015, PÅ™emysl Janouch * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ // A table of all attributes we use for output #define ATTR_TABLE(XX) \ XX( PROMPT, "prompt", "Terminal attributes for the prompt" ) \ XX( RESET, "reset", "String to reset terminal attributes" ) \ XX( WARNING, "warning", "Terminal attributes for warnings" ) \ XX( ERROR, "error", "Terminal attributes for errors" ) \ XX( EXTERNAL, "external", "Terminal attributes for external lines" ) \ XX( TIMESTAMP, "timestamp", "Terminal attributes for timestamps" ) \ XX( HIGHLIGHT, "highlight", "Terminal attributes for highlights" ) \ XX( ACTION, "action", "Terminal attributes for user actions" ) \ XX( USERHOST, "userhost", "Terminal attributes for user@host" ) \ XX( JOIN, "join", "Terminal attributes for joins" ) \ XX( PART, "part", "Terminal attributes for parts" ) enum { #define XX(x, y, z) ATTR_ ## x, ATTR_TABLE (XX) #undef XX ATTR_COUNT }; // User data for logger functions to enable formatted logging #define print_fatal_data ((void *) ATTR_ERROR) #define print_error_data ((void *) ATTR_ERROR) #define print_warning_data ((void *) ATTR_WARNING) #include "config.h" #define PROGRAM_NAME "degesch" #include "common.c" #include "kike-replies.c" #include #include #include #include #include #include #ifndef TIOCGWINSZ #include #endif // ! TIOCGWINSZ #include #include // Literally cancer #undef lines #undef columns #ifdef HAVE_READLINE #include #include #endif // HAVE_READLINE #ifdef HAVE_EDITLINE #include #endif // HAVE_EDITLINE /// Some arbitrary limit for the history file #define HISTORY_LIMIT 10000 /// Characters that separate words #define WORD_BREAKING_CHARS " \f\n\r\t\v" // --- User interface ---------------------------------------------------------- // I'm not sure which one of these backends is worse: whether it's GNU Readline // or BSD Editline. They both have their own annoying problems. struct input_buffer { #ifdef HAVE_READLINE HISTORY_STATE *history; ///< Saved history state char *saved_line; ///< Saved line content int saved_mark; ///< Saved mark #elif defined HAVE_EDITLINE HistoryW *history; ///< The history object wchar_t *saved_line; ///< Saved line content int saved_len; ///< Length of the saved line #endif // HAVE_EDITLINE int saved_point; ///< Saved cursor position }; static struct input_buffer * input_buffer_new (void) { struct input_buffer *self = xcalloc (1, sizeof *self); #ifdef HAVE_EDITLINE self->history = history_winit (); HistEventW ev; history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT); #endif // HAVE_EDITLINE return self; } static void input_buffer_destroy (struct input_buffer *self) { #ifdef HAVE_READLINE // Can't really free "history" from here #elif defined HAVE_EDITLINE history_wend (self->history); #endif // HAVE_EDITLINE free (self->saved_line); free (self); } struct input { bool active; ///< Are we a thing? #if defined HAVE_READLINE char *saved_line; ///< Saved line content int saved_point; ///< Saved cursor position int saved_mark; ///< Saved mark #elif defined HAVE_EDITLINE EditLine *editline; ///< The EditLine object char *(*saved_prompt) (EditLine *); ///< Saved prompt function char saved_char; ///< Saved char for the prompt #endif // HAVE_EDITLINE char *prompt; ///< The prompt we use int prompt_shown; ///< Whether the prompt is shown now struct input_buffer *current; ///< Current input buffer }; static void input_init (struct input *self) { memset (self, 0, sizeof *self); } static void input_free (struct input *self) { #ifdef HAVE_READLINE free (self->saved_line); #endif // HAVE_READLINE free (self->prompt); } // --- GNU Readline ------------------------------------------------------------ #ifdef HAVE_READLINE #define INPUT_START_IGNORE RL_PROMPT_START_IGNORE #define INPUT_END_IGNORE RL_PROMPT_END_IGNORE #define input_ding(self) rl_ding () static void input_on_terminal_resized (struct input *self) { (void) self; // This fucks up big time on terminals with automatic wrapping such as // rxvt-unicode or newer VTE when the current line overflows, however we // can't do much about that rl_resize_terminal (); } static void input_on_readable (struct input *self) { (void) self; rl_callback_read_char (); } static void input_set_prompt (struct input *self, char *prompt) { free (self->prompt); self->prompt = prompt; // First reset the prompt to work around a bug in readline rl_set_prompt (""); if (self->prompt_shown) rl_redisplay (); rl_set_prompt (self->prompt); if (self->prompt_shown) rl_redisplay (); } static void input_erase (struct input *self) { (void) self; rl_set_prompt (""); rl_replace_line ("", 0); rl_point = rl_mark = 0; rl_redisplay (); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static int app_readline_init (void); static void on_readline_input (char *line); static char **app_readline_completion (const char *text, int start, int end); static void input_start (struct input *self, const char *program_name) { using_history (); // This can cause memory leaks, or maybe even a segfault. Funny, eh? stifle_history (HISTORY_LIMIT); const char *slash = strrchr (program_name, '/'); rl_readline_name = slash ? ++slash : program_name; rl_startup_hook = app_readline_init; rl_catch_sigwinch = false; rl_basic_word_break_characters = WORD_BREAKING_CHARS; rl_completer_word_break_characters = NULL; rl_attempted_completion_function = app_readline_completion; hard_assert (self->prompt != NULL); rl_callback_handler_install (self->prompt, on_readline_input); self->prompt_shown = 1; self->active = true; } static void input_stop (struct input *self) { if (self->prompt_shown > 0) input_erase (self); // This is okay as long as we're not called from within readline rl_callback_handler_remove (); self->active = false; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // The following part shows you why it's not a good idea to use // GNU Readline for this kind of software. Or for anything else, really. static void input_save_buffer (struct input *self, struct input_buffer *buffer) { (void) self; buffer->history = history_get_history_state (); buffer->saved_line = rl_copy_text (0, rl_end); buffer->saved_point = rl_point; buffer->saved_mark = rl_mark; } static void input_restore_buffer (struct input *self, struct input_buffer *buffer) { // Restore the target buffer's history if (buffer->history) { // history_get_history_state() just allocates a new HISTORY_STATE // and fills it with its current internal data. We don't need that // shell anymore after reviving it. history_set_history_state (buffer->history); free (buffer->history); buffer->history = NULL; } else { // This should get us a clean history while keeping the flags. // Note that we've either saved the previous history entries, or we've // cleared them altogether, so there should be nothing to leak. HISTORY_STATE *state = history_get_history_state (); state->offset = state->length = state->size = 0; history_set_history_state (state); free (state); } // Try to restore the target buffer's readline state if (buffer->saved_line) { rl_replace_line (buffer->saved_line, 0); rl_point = buffer->saved_point; rl_mark = buffer->saved_mark; free (buffer->saved_line); buffer->saved_line = NULL; if (self->prompt_shown) rl_redisplay (); } } static void input_switch_buffer (struct input *self, struct input_buffer *buffer) { // There could possibly be occurences of the current undo list in some // history entry. We either need to free the undo list, or move it // somewhere else to load back later, as the buffer we're switching to // has its own history state. rl_free_undo_list (); // Save this buffer's history so that it's independent for each buffer if (self->current) input_save_buffer (self, self->current); else // Just throw it away; there should always be an active buffer however #if RL_READLINE_VERSION >= 0x0603 rl_clear_history (); #else // RL_READLINE_VERSION < 0x0603 // At least something... this may leak undo entries clear_history (); #endif // RL_READLINE_VERSION < 0x0603 input_restore_buffer (self, buffer); self->current = buffer; } static void input_destroy_buffer (struct input *self, struct input_buffer *buffer) { (void) self; // rl_clear_history, being the only way I know of to get rid of the complete // history including attached data, is a pretty recent addition. *sigh* #if RL_READLINE_VERSION >= 0x0603 if (buffer->history) { // See input_switch_buffer() for why we need to do this BS rl_free_undo_list (); // This is probably the only way we can free the history fully HISTORY_STATE *state = history_get_history_state (); history_set_history_state (buffer->history); free (buffer->history); rl_clear_history (); history_set_history_state (state); free (state); } #endif // RL_READLINE_VERSION input_buffer_destroy (buffer); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_save (struct input *self) { hard_assert (!self->saved_line); self->saved_point = rl_point; self->saved_mark = rl_mark; self->saved_line = rl_copy_text (0, rl_end); } static void input_restore (struct input *self) { hard_assert (self->saved_line); rl_set_prompt (self->prompt); rl_replace_line (self->saved_line, 0); rl_point = self->saved_point; rl_mark = self->saved_mark; free (self->saved_line); self->saved_line = NULL; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_hide (struct input *self) { if (!self->active || self->prompt_shown-- < 1) return; input_save (self); input_erase (self); } static void input_show (struct input *self) { if (!self->active || ++self->prompt_shown < 1) return; input_restore (self); rl_redisplay (); } #endif // HAVE_READLINE // --- BSD Editline ------------------------------------------------------------ #ifdef HAVE_EDITLINE #define INPUT_START_IGNORE '\x01' #define INPUT_END_IGNORE '\x01' static void app_editline_init (struct input *self); static void input_ding (struct input *self) { (void) self; // XXX: this isn't probably very portable; // we could use "bell" from terminfo but that creates a dependency write (STDOUT_FILENO, "\a", 1); } static void input_on_terminal_resized (struct input *self) { el_resize (self->editline); } static void input_redisplay (struct input *self) { // See rl_redisplay() // The character is VREPRINT (usually C-r) // TODO: read it from terminal info // XXX: could we potentially break UTF-8 with this? char x[] = { ('R' - 'A' + 1), 0 }; el_push (self->editline, x); // We have to do this or it gets stuck and nothing is done (void) el_gets (self->editline, NULL); } static void input_set_prompt (struct input *self, char *prompt) { free (self->prompt); self->prompt = prompt; if (self->prompt_shown) input_redisplay (self); } static char * input_make_prompt (EditLine *editline) { struct input *self; el_get (editline, EL_CLIENTDATA, &self); if (!self->prompt) return ""; return self->prompt; } static char * input_make_empty_prompt (EditLine *editline) { (void) editline; return ""; } static void input_erase (struct input *self) { const LineInfoW *info = el_wline (self->editline); int len = info->lastchar - info->buffer; int point = info->cursor - info->buffer; el_cursor (self->editline, len - point); el_wdeletestr (self->editline, len); // XXX: this doesn't seem to save the escape character el_get (self->editline, EL_PROMPT, &self->saved_prompt, &self->saved_char); el_set (self->editline, EL_PROMPT, input_make_empty_prompt); input_redisplay (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_start (struct input *self, const char *program_name) { self->editline = el_init (program_name, stdin, stdout, stderr); el_set (self->editline, EL_CLIENTDATA, self); el_set (self->editline, EL_PROMPT_ESC, input_make_prompt, INPUT_START_IGNORE); el_set (self->editline, EL_SIGNAL, false); el_set (self->editline, EL_UNBUFFERED, true); el_set (self->editline, EL_EDITOR, "emacs"); // No, editline, it's not supposed to kill the entire line el_set (self->editline, EL_BIND, "^W", "ed-delete-prev-word", NULL); // Just what are you doing? el_set (self->editline, EL_BIND, "^U", "vi-kill-line-prev", NULL); app_editline_init (self); self->prompt_shown = 1; self->active = true; } static void input_stop (struct input *self) { if (self->prompt_shown > 0) input_erase (self); el_end (self->editline); self->editline = NULL; self->active = false; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_save_buffer (struct input *self, struct input_buffer *buffer) { const LineInfoW *info = el_wline (self->editline); int len = info->lastchar - info->buffer; int point = info->cursor - info->buffer; wchar_t *line = calloc (sizeof *info->buffer, len + 1); memcpy (line, info->buffer, sizeof *info->buffer * len); el_cursor (self->editline, len - point); el_wdeletestr (self->editline, len); buffer->saved_line = line; buffer->saved_point = point; buffer->saved_len = len; } static void input_restore_buffer (struct input *self, struct input_buffer *buffer) { if (buffer->saved_line) { el_winsertstr (self->editline, buffer->saved_line); el_cursor (self->editline, -(buffer->saved_len - buffer->saved_point)); free (buffer->saved_line); buffer->saved_line = NULL; } } static void input_switch_buffer (struct input *self, struct input_buffer *buffer) { if (self->current) input_save_buffer (self, self->current); input_restore_buffer (self, buffer); el_wset (self->editline, EL_HIST, history, buffer->history); self->current = buffer; } static void input_destroy_buffer (struct input *self, struct input_buffer *buffer) { (void) self; input_buffer_destroy (buffer); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_save (struct input *self) { if (self->current) input_save_buffer (self, self->current); } static void input_restore (struct input *self) { if (self->current) input_restore_buffer (self, self->current); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_hide (struct input *self) { if (!self->active || self->prompt_shown-- < 1) return; input_save (self); input_erase (self); } static void input_show (struct input *self) { if (!self->active || ++self->prompt_shown < 1) return; input_restore (self); // Would have used "saved_char" but it doesn't seem to work. // And it doesn't even when it does anyway (it seems to just strip it). el_set (self->editline, EL_PROMPT_ESC, input_make_prompt, INPUT_START_IGNORE); input_redisplay (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void input_on_readable (struct input *self) { // We bind the return key to process it how we need to // el_gets() with EL_UNBUFFERED doesn't work with UTF-8, // we must use the wide-character interface int count = 0; const wchar_t *buf = el_wgets (self->editline, &count); if (!buf || count-- <= 0) return; // The character is VEOF (usually C-d) // TODO: read it from terminal info if (count == 0 && buf[0] == ('D' - 'A' + 1)) { el_deletestr (self->editline, 1); input_redisplay (self); input_ding (self); } } #endif // HAVE_EDITLINE // --- Application data -------------------------------------------------------- // All text stored in our data structures is encoded in UTF-8. // Or at least should be. The exception is IRC identifiers. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // We need a few reference countable objects with support // for both strong and weak references /// Callback just before a reference counted object is destroyed typedef void (*destroy_cb_fn) (void *object, void *user_data); #define REF_COUNTABLE_HEADER \ size_t ref_count; /**< Reference count */ \ destroy_cb_fn on_destroy; /**< To remove any weak references */ \ void *user_data; /**< User data for callbacks */ #define REF_COUNTABLE_METHODS(name) \ static struct name * \ name ## _ref (struct name *self) \ { \ self->ref_count++; \ return self; \ } \ \ static void \ name ## _unref (struct name *self) \ { \ if (--self->ref_count) \ return; \ if (self->on_destroy) \ self->on_destroy (self, self->user_data); \ name ## _destroy (self); \ } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct user_channel { LIST_HEADER (struct user_channel) struct channel *channel; ///< Reference to channel }; static struct user_channel * user_channel_new (void) { struct user_channel *self = xcalloc (1, sizeof *self); return self; } static void user_channel_destroy (struct user_channel *self) { // The "channel" reference is weak and this object should get // destroyed whenever the user stops being in the channel. free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // We keep references to user information in channels and buffers, // and weak references in the name lookup table. struct user { REF_COUNTABLE_HEADER // TODO: eventually a reference to the server char *nickname; ///< Literal nickname // TODO: write code to poll for the away status bool away; ///< User is away struct user_channel *channels; ///< Channels the user is on }; static struct user * user_new (void) { struct user *self = xcalloc (1, sizeof *self); self->ref_count = 1; return self; } static void user_destroy (struct user *self) { free (self->nickname); LIST_FOR_EACH (struct user_channel, iter, self->channels) user_channel_destroy (iter); free (self); } REF_COUNTABLE_METHODS (user) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct channel_user { LIST_HEADER (struct channel_user) struct user *user; ///< Reference to user char *modes; ///< Op/voice/... characters }; static struct channel_user * channel_user_new (void) { struct channel_user *self = xcalloc (1, sizeof *self); return self; } static void channel_user_destroy (struct channel_user *self) { user_unref (self->user); free (self->modes); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // We keep references to channels in their buffers, // and weak references in their users and the name lookup table. // XXX: this doesn't really have to be reference countable struct channel { REF_COUNTABLE_HEADER // TODO: eventually a reference to the server char *name; ///< Channel name char *mode; ///< Channel mode char *topic; ///< Channel topic struct channel_user *users; ///< Channel users struct str_vector names_buf; ///< Buffer for RPL_NAMREPLY }; static struct channel * channel_new (void) { struct channel *self = xcalloc (1, sizeof *self); self->ref_count = 1; str_vector_init (&self->names_buf); return self; } static void channel_destroy (struct channel *self) { free (self->name); free (self->mode); free (self->topic); // Owner has to make sure we have no users by now hard_assert (!self->users); str_vector_free (&self->names_buf); free (self); } REF_COUNTABLE_METHODS (channel) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum buffer_line_flags { BUFFER_LINE_HIGHLIGHT = 1 << 0 ///< The user was highlighted by this }; enum buffer_line_type { BUFFER_LINE_PRIVMSG, ///< PRIVMSG BUFFER_LINE_ACTION, ///< PRIVMSG ACTION BUFFER_LINE_NOTICE, ///< NOTICE BUFFER_LINE_JOIN, ///< JOIN BUFFER_LINE_PART, ///< PART BUFFER_LINE_KICK, ///< KICK BUFFER_LINE_NICK, ///< NICK BUFFER_LINE_TOPIC, ///< TOPIC BUFFER_LINE_QUIT, ///< QUIT BUFFER_LINE_STATUS, ///< Whatever status messages BUFFER_LINE_ERROR ///< Whatever error messages }; struct buffer_line_args { char *who; ///< Name of the origin or NULL (user) char *object; ///< Object of action char *text; ///< Text of message char *reason; ///< Reason for PART, KICK, QUIT }; struct buffer_line { LIST_HEADER (struct buffer_line) // We use the "type" and "flags" mostly just as formatting hints enum buffer_line_type type; ///< Type of the event int flags; ///< Flags time_t when; ///< Time of the event struct buffer_line_args args; ///< Arguments }; struct buffer_line * buffer_line_new (void) { struct buffer_line *self = xcalloc (1, sizeof *self); return self; } static void buffer_line_destroy (struct buffer_line *self) { free (self->args.who); free (self->args.object); free (self->args.text); free (self->args.reason); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum buffer_type { BUFFER_GLOBAL, ///< Global information BUFFER_SERVER, ///< Server-related messages BUFFER_CHANNEL, ///< Channels BUFFER_PM ///< Private messages (query) }; struct buffer { LIST_HEADER (struct buffer) enum buffer_type type; ///< Type of the buffer char *name; ///< The name of the buffer struct input_buffer *input_data; ///< User interface data // Buffer contents: struct buffer_line *lines; ///< All lines in this buffer struct buffer_line *lines_tail; ///< The tail of buffer lines unsigned lines_count; ///< How many lines we have unsigned unseen_messages_count; ///< # messages since last visited // Origin information: struct server *server; ///< Reference to server struct channel *channel; ///< Reference to channel struct user *user; ///< Reference to user }; static struct buffer * buffer_new (void) { struct buffer *self = xcalloc (1, sizeof *self); self->input_data = input_buffer_new (); return self; } static void buffer_destroy (struct buffer *self) { free (self->name); if (self->input_data) input_buffer_destroy (self->input_data); LIST_FOR_EACH (struct buffer_line, iter, self->lines) buffer_line_destroy (iter); if (self->user) user_unref (self->user); if (self->channel) channel_unref (self->channel); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct server { struct app_context *ctx; ///< Application context int irc_fd; ///< Socket FD of the server struct str read_buffer; ///< Input yet to be processed struct poller_fd irc_event; ///< IRC FD event bool irc_ready; ///< Whether we may send messages now SSL_CTX *ssl_ctx; ///< SSL context SSL *ssl; ///< SSL connection // TODO: an output queue to prevent excess floods (this will be needed // especially for away status polling) // XXX: there can be buffers for non-existent users // TODO: initialize key_strxfrm according to server properties; // note that collisions may arise on reconnecting // TODO: when disconnected, get rid of all users everywhere; // maybe also broadcast all buffers about the disconnection event // TODO: when getting connected again, rejoin all current channels struct buffer *buffer; ///< The buffer for this server struct str_map irc_users; ///< IRC user data struct str_map irc_channels; ///< IRC channel data struct str_map irc_buffer_map; ///< Maps IRC identifiers to buffers struct user *irc_user; ///< Our own user char *irc_user_mode; ///< Our current user mode char *irc_user_host; ///< Our current user@host // Events: struct poller_timer ping_tmr; ///< We should send a ping struct poller_timer timeout_tmr; ///< Connection seems to be dead struct poller_timer reconnect_tmr; ///< We should reconnect now }; static void on_irc_ping_timeout (void *user_data); static void on_irc_timeout (void *user_data); static void on_irc_reconnect_timeout (void *user_data); static void server_init (struct server *self, struct poller *poller) { self->irc_fd = -1; str_init (&self->read_buffer); self->irc_ready = false; str_map_init (&self->irc_users); self->irc_users.key_xfrm = irc_strxfrm; str_map_init (&self->irc_channels); self->irc_channels.key_xfrm = irc_strxfrm; str_map_init (&self->irc_buffer_map); self->irc_buffer_map.key_xfrm = irc_strxfrm; poller_timer_init (&self->timeout_tmr, poller); self->timeout_tmr.dispatcher = on_irc_timeout; self->timeout_tmr.user_data = self; poller_timer_init (&self->ping_tmr, poller); self->ping_tmr.dispatcher = on_irc_ping_timeout; self->ping_tmr.user_data = self; poller_timer_init (&self->reconnect_tmr, poller); self->reconnect_tmr.dispatcher = on_irc_reconnect_timeout; self->reconnect_tmr.user_data = self; } static void server_free (struct server *self) { if (self->irc_fd != -1) { xclose (self->irc_fd); poller_fd_reset (&self->irc_event); } str_free (&self->read_buffer); if (self->ssl) SSL_free (self->ssl); if (self->ssl_ctx) SSL_CTX_free (self->ssl_ctx); if (self->irc_user) user_unref (self->irc_user); free (self->irc_user_mode); free (self->irc_user_host); str_map_free (&self->irc_users); str_map_free (&self->irc_channels); str_map_free (&self->irc_buffer_map); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct app_context { // Configuration: struct config config; ///< Program configuration char *attrs[ATTR_COUNT]; ///< Terminal attributes bool no_colors; ///< Colour output mode bool reconnect; ///< Whether to reconnect on conn. fail. unsigned long reconnect_delay; ///< Reconnect delay in seconds bool isolate_buffers; ///< Isolate global/server buffers struct server server; ///< Our only server so far // Events: struct poller_fd tty_event; ///< Terminal input event struct poller_fd signal_event; ///< Signal FD event struct poller poller; ///< Manages polled descriptors bool quitting; ///< User requested quitting bool polling; ///< The event loop is running // Buffers: struct buffer *buffers; ///< All our buffers in order struct buffer *buffers_tail; ///< The tail of our buffers struct buffer *last_buffer; ///< Last used buffer // XXX: when we go multiserver, there will be collisions // TODO: make buffer names unique like weechat does struct str_map buffers_by_name; ///< Excludes GLOBAL and SERVER struct buffer *global_buffer; ///< The global buffer struct buffer *current_buffer; ///< The current buffer // TODO: So that we always output proper date change messages time_t last_displayed_msg_time; ///< Time of last displayed message // Terminal: iconv_t term_to_utf8; ///< Terminal encoding to UTF-8 iconv_t term_from_utf8; ///< UTF-8 to terminal encoding iconv_t latin1_to_utf8; ///< ISO Latin 1 to UTF-8 struct input input; ///< User interface } *g_ctx; static void app_context_init (struct app_context *self) { memset (self, 0, sizeof *self); config_init (&self->config); poller_init (&self->poller); server_init (&self->server, &self->poller); self->server.ctx = self; str_map_init (&self->buffers_by_name); self->buffers_by_name.key_xfrm = irc_strxfrm; self->last_displayed_msg_time = time (NULL); char *encoding = nl_langinfo (CODESET); #ifdef __linux__ encoding = xstrdup_printf ("%s//TRANSLIT", encoding); #else // ! __linux__ encoding = xstrdup (encoding); #endif // ! __linux__ if ((self->term_from_utf8 = iconv_open (encoding, "UTF-8")) == (iconv_t) -1 || (self->latin1_to_utf8 = iconv_open ("UTF-8", "ISO-8859-1")) == (iconv_t) -1 || (self->term_to_utf8 = iconv_open ("UTF-8", nl_langinfo (CODESET))) == (iconv_t) -1) exit_fatal ("creating the UTF-8 conversion object failed: %s", strerror (errno)); free (encoding); input_init (&self->input); } static void app_context_free (struct app_context *self) { config_free (&self->config); for (size_t i = 0; i < ATTR_COUNT; i++) free (self->attrs[i]); // FIXME: this doesn't free the history state LIST_FOR_EACH (struct buffer, iter, self->buffers) buffer_destroy (iter); str_map_free (&self->buffers_by_name); server_free (&self->server); poller_free (&self->poller); iconv_close (self->latin1_to_utf8); iconv_close (self->term_from_utf8); iconv_close (self->term_to_utf8); input_free (&self->input); } static void refresh_prompt (struct app_context *ctx); static char *irc_cut_nickname (const char *prefix); static const char *irc_find_userhost (const char *prefix); // --- Configuration ----------------------------------------------------------- // TODO: eventually add "on_change" callbacks static bool config_validate_nonjunk_string (const struct config_item_ *item, struct error **e) { if (item->type == CONFIG_ITEM_NULL) return true; hard_assert (config_item_type_is_string (item->type)); for (size_t i = 0; i < item->value.string.len; i++) { // Not even a tabulator unsigned char c = item->value.string.str[i]; if (c < 32) { error_set (e, "control characters are not allowed"); return false; } } return true; } static bool config_validate_nonnegative (const struct config_item_ *item, struct error **e) { if (item->type == CONFIG_ITEM_NULL) return true; hard_assert (item->type == CONFIG_ITEM_INTEGER); if (item->value.integer >= 0) return true; error_set (e, "must be non-negative"); return false; } struct config_schema g_config_server[] = { { .name = "nickname", .comment = "IRC nickname", .type = CONFIG_ITEM_STRING, .validate = config_validate_nonjunk_string }, { .name = "username", .comment = "IRC user name", .type = CONFIG_ITEM_STRING, .validate = config_validate_nonjunk_string }, { .name = "realname", .comment = "IRC real name/e-mail", .type = CONFIG_ITEM_STRING, .validate = config_validate_nonjunk_string }, { .name = "irc_host", .comment = "Address of the IRC server", .type = CONFIG_ITEM_STRING, .validate = config_validate_nonjunk_string }, { .name = "irc_port", .comment = "Port of the IRC server", .type = CONFIG_ITEM_INTEGER, .validate = config_validate_nonnegative, .default_ = "6667" }, { .name = "ssl", .comment = "Whether to use SSL/TLS", .type = CONFIG_ITEM_BOOLEAN, .default_ = "off" }, { .name = "ssl_cert", .comment = "Client SSL certificate (PEM)", .type = CONFIG_ITEM_STRING }, { .name = "ssl_verify", .comment = "Whether to verify certificates", .type = CONFIG_ITEM_BOOLEAN, .default_ = "on" }, { .name = "ssl_ca_file", .comment = "OpenSSL CA bundle file", .type = CONFIG_ITEM_STRING }, { .name = "ssl_ca_path", .comment = "OpenSSL CA bundle path", .type = CONFIG_ITEM_STRING }, { .name = "autojoin", .comment = "Channels to join on start", .type = CONFIG_ITEM_STRING_ARRAY, .validate = config_validate_nonjunk_string }, { .name = "reconnect", .comment = "Whether to reconnect on error", .type = CONFIG_ITEM_BOOLEAN, .default_ = "on" }, { .name = "reconnect_delay", .comment = "Time between reconnecting", .type = CONFIG_ITEM_INTEGER, .default_ = "5" }, { .name = "socks_host", .comment = "Address of a SOCKS 4a/5 proxy", .type = CONFIG_ITEM_STRING, .validate = config_validate_nonjunk_string }, { .name = "socks_port", .comment = "SOCKS port number", .type = CONFIG_ITEM_INTEGER, .validate = config_validate_nonnegative, .default_ = "1080" }, { .name = "socks_username", .comment = "SOCKS auth. username", .type = CONFIG_ITEM_STRING }, { .name = "socks_password", .comment = "SOCKS auth. password", .type = CONFIG_ITEM_STRING }, {} }; struct config_schema g_config_behaviour[] = { { .name = "isolate_buffers", .comment = "Don't leak messages from the server and global buffers", .type = CONFIG_ITEM_BOOLEAN, .default_ = "off" }, {} }; struct config_schema g_config_attributes[] = { #define XX(x, y, z) { .name = y, .comment = z, .type = CONFIG_ITEM_STRING }, ATTR_TABLE (XX) #undef XX {} }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void load_config_server (struct config_item_ *subtree, void *user_data) { (void) user_data; // This will eventually iterate over the object and create servers config_schema_apply_to_object (g_config_server, subtree); } static void load_config_behaviour (struct config_item_ *subtree, void *user_data) { (void) user_data; config_schema_apply_to_object (g_config_behaviour, subtree); } static void load_config_attributes (struct config_item_ *subtree, void *user_data) { (void) user_data; config_schema_apply_to_object (g_config_attributes, subtree); } static void register_config_modules (struct app_context *ctx) { struct config *config = &ctx->config; config_register_module (config, "server", load_config_server, ctx); config_register_module (config, "behaviour", load_config_behaviour, ctx); config_register_module (config, "attributes", load_config_attributes, ctx); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static const char * get_config_string (struct app_context *ctx, const char *key) { struct config_item_ *item = config_item_get (ctx->config.root, key, NULL); hard_assert (item); if (item->type == CONFIG_ITEM_NULL) return NULL; hard_assert (config_item_type_is_string (item->type)); return item->value.string.str; } static bool set_config_string (struct app_context *ctx, const char *key, const char *value) { struct config_item_ *item = config_item_get (ctx->config.root, key, NULL); hard_assert (item); struct str s; str_init (&s); str_append (&s, value); struct config_item_ *new_ = config_item_string (&s); str_free (&s); struct error *e = NULL; if (config_item_set_from (item, new_, &e)) return true; config_item_destroy (new_); print_error ("couldn't set `%s' in configuration: %s", key, e->message); error_free (e); return false; } static int64_t get_config_integer (struct app_context *ctx, const char *key) { struct config_item_ *item = config_item_get (ctx->config.root, key, NULL); hard_assert (item && item->type == CONFIG_ITEM_INTEGER); return item->value.integer; } static bool get_config_boolean (struct app_context *ctx, const char *key) { struct config_item_ *item = config_item_get (ctx->config.root, key, NULL); hard_assert (item && item->type == CONFIG_ITEM_BOOLEAN); return item->value.boolean; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static char * write_configuration_file (const struct str *data, struct error **e) { struct str path; str_init (&path); get_xdg_home_dir (&path, "XDG_CONFIG_HOME", ".config"); str_append (&path, "/" PROGRAM_NAME); if (!mkdir_with_parents (path.str, e)) goto error; str_append (&path, "/" PROGRAM_NAME ".conf"); FILE *fp = fopen (path.str, "w"); if (!fp) { error_set (e, "could not open `%s' for writing: %s", path.str, strerror (errno)); goto error; } errno = 0; fwrite (data->str, data->len, 1, fp); fclose (fp); if (errno) { error_set (e, "writing to `%s' failed: %s", path.str, strerror (errno)); goto error; } return str_steal (&path); error: str_free (&path); return NULL; } static void serialize_configuration (struct app_context *ctx, struct str *output) { str_append (output, "# " PROGRAM_NAME " " PROGRAM_VERSION " configuration file\n" "#\n" "# Relative paths are searched for in ${XDG_CONFIG_HOME:-~/.config}\n" "# /" PROGRAM_NAME " as well as in $XDG_CONFIG_DIRS/" PROGRAM_NAME "\n" "#\n" "# Everything is in UTF-8. Any custom comments will be overwritten.\n" "\n"); config_item_write (ctx->config.root, true, output); } // --- Attributed output ------------------------------------------------------- static struct { bool initialized; ///< Terminal is available bool stdout_is_tty; ///< `stdout' is a terminal bool stderr_is_tty; ///< `stderr' is a terminal char *color_set_fg[8]; ///< Codes to set the foreground colour char *color_set_bg[8]; ///< Codes to set the background colour int lines; ///< Number of lines int columns; ///< Number of columns } g_terminal; static void update_screen_size (void) { #ifdef TIOCGWINSZ if (!g_terminal.stdout_is_tty) return; struct winsize size; if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size)) { char *row = getenv ("LINES"); char *col = getenv ("COLUMNS"); unsigned long tmp; g_terminal.lines = (row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row; g_terminal.columns = (col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col; } #endif // TIOCGWINSZ } static bool init_terminal (void) { int tty_fd = -1; if ((g_terminal.stderr_is_tty = isatty (STDERR_FILENO))) tty_fd = STDERR_FILENO; if ((g_terminal.stdout_is_tty = isatty (STDOUT_FILENO))) tty_fd = STDOUT_FILENO; int err; if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR) return false; // Make sure all terminal features used by us are supported if (!set_a_foreground || !set_a_background || !enter_bold_mode || !exit_attribute_mode) { del_curterm (cur_term); return false; } g_terminal.lines = tigetnum ("lines"); g_terminal.columns = tigetnum ("cols"); update_screen_size (); for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set_fg); i++) { g_terminal.color_set_fg[i] = xstrdup (tparm (set_a_foreground, i, 0, 0, 0, 0, 0, 0, 0, 0)); g_terminal.color_set_bg[i] = xstrdup (tparm (set_a_background, i, 0, 0, 0, 0, 0, 0, 0, 0)); } return g_terminal.initialized = true; } static void free_terminal (void) { if (!g_terminal.initialized) return; for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set_fg); i++) { free (g_terminal.color_set_fg[i]); free (g_terminal.color_set_bg[i]); } del_curterm (cur_term); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - typedef int (*terminal_printer_fn) (int); static int putchar_stderr (int c) { return fputc (c, stderr); } static terminal_printer_fn get_attribute_printer (FILE *stream) { if (stream == stdout && g_terminal.stdout_is_tty) return putchar; if (stream == stderr && g_terminal.stderr_is_tty) return putchar_stderr; return NULL; } static void vprint_attributed (struct app_context *ctx, FILE *stream, intptr_t attribute, const char *fmt, va_list ap) { terminal_printer_fn printer = get_attribute_printer (stream); if (!attribute) printer = NULL; if (printer) tputs (ctx->attrs[attribute], 1, printer); vfprintf (stream, fmt, ap); if (printer) tputs (ctx->attrs[ATTR_RESET], 1, printer); } static void print_attributed (struct app_context *ctx, FILE *stream, intptr_t attribute, const char *fmt, ...) { va_list ap; va_start (ap, fmt); vprint_attributed (ctx, stream, attribute, fmt, ap); va_end (ap); } static void log_message_attributed (void *user_data, const char *quote, const char *fmt, va_list ap) { FILE *stream = stderr; struct app_context *ctx = g_ctx; input_hide (&ctx->input); print_attributed (ctx, stream, (intptr_t) user_data, "%s", quote); vprint_attributed (ctx, stream, (intptr_t) user_data, fmt, ap); fputs ("\n", stream); input_show (&ctx->input); } static void init_attribute (struct app_context *ctx, int id, const char *default_) { static const char *table[ATTR_COUNT] = { #define XX(x, y, z) [ATTR_ ## x] = "attributes." y, ATTR_TABLE (XX) #undef XX }; const char *user = get_config_string (ctx, table[id]); if (user) ctx->attrs[id] = xstrdup (user); else ctx->attrs[id] = xstrdup (default_); } static void init_colors (struct app_context *ctx) { bool have_ti = init_terminal (); #define INIT_ATTR(id, ti) init_attribute (ctx, ATTR_ ## id, have_ti ? (ti) : "") INIT_ATTR (PROMPT, enter_bold_mode); INIT_ATTR (RESET, exit_attribute_mode); INIT_ATTR (WARNING, g_terminal.color_set_fg[COLOR_YELLOW]); INIT_ATTR (ERROR, g_terminal.color_set_fg[COLOR_RED]); INIT_ATTR (EXTERNAL, g_terminal.color_set_fg[COLOR_WHITE]); INIT_ATTR (TIMESTAMP, g_terminal.color_set_fg[COLOR_WHITE]); INIT_ATTR (ACTION, g_terminal.color_set_fg[COLOR_RED]); INIT_ATTR (USERHOST, g_terminal.color_set_fg[COLOR_CYAN]); INIT_ATTR (JOIN, g_terminal.color_set_fg[COLOR_GREEN]); INIT_ATTR (PART, g_terminal.color_set_fg[COLOR_RED]); char *highlight = xstrdup_printf ("%s%s%s", g_terminal.color_set_fg[COLOR_YELLOW], g_terminal.color_set_bg[COLOR_MAGENTA], enter_bold_mode); INIT_ATTR (HIGHLIGHT, highlight); free (highlight); #undef INIT_ATTR if (ctx->no_colors) { g_terminal.stdout_is_tty = false; g_terminal.stderr_is_tty = false; } g_log_message_real = log_message_attributed; } // --- Signals ----------------------------------------------------------------- static int g_signal_pipe[2]; ///< A pipe used to signal... signals /// Program termination has been requested by a signal static volatile sig_atomic_t g_termination_requested; /// The window has changed in size static volatile sig_atomic_t g_winch_received; static void sigterm_handler (int signum) { (void) signum; g_termination_requested = true; int original_errno = errno; if (write (g_signal_pipe[1], "t", 1) == -1) soft_assert (errno == EAGAIN); errno = original_errno; } static void sigwinch_handler (int signum) { (void) signum; g_winch_received = true; int original_errno = errno; if (write (g_signal_pipe[1], "w", 1) == -1) soft_assert (errno == EAGAIN); errno = original_errno; } static void setup_signal_handlers (void) { if (pipe (g_signal_pipe) == -1) exit_fatal ("%s: %s", "pipe", strerror (errno)); set_cloexec (g_signal_pipe[0]); set_cloexec (g_signal_pipe[1]); // So that the pipe cannot overflow; it would make write() block within // the signal handler, which is something we really don't want to happen. // The same holds true for read(). set_blocking (g_signal_pipe[0], false); set_blocking (g_signal_pipe[1], false); signal (SIGPIPE, SIG_IGN); struct sigaction sa; sa.sa_flags = SA_RESTART; sa.sa_handler = sigwinch_handler; sigemptyset (&sa.sa_mask); if (sigaction (SIGWINCH, &sa, NULL) == -1) exit_fatal ("sigaction: %s", strerror (errno)); sa.sa_handler = sigterm_handler; if (sigaction (SIGINT, &sa, NULL) == -1 || sigaction (SIGTERM, &sa, NULL) == -1) exit_fatal ("sigaction: %s", strerror (errno)); } // --- Output formatter -------------------------------------------------------- // This complicated piece of code makes attributed text formatting simple. // We use a printf-inspired syntax to push attributes and text to the object, // then flush it either to a terminal, or a log file with formatting stripped. // // Format strings use a #-quoted notation, to differentiate from printf: // #s inserts a string // #d inserts a signed integer; also supports the # and #0 notation // // #a inserts named attributes (auto-resets) // #r resets terminal attributes // #c sets foreground color // #C sets background color enum formatter_item_type { FORMATTER_ITEM_TEXT, ///< Text FORMATTER_ITEM_ATTR, ///< Formatting attributes FORMATTER_ITEM_FG_COLOR, ///< Foreground color FORMATTER_ITEM_BG_COLOR ///< Background color }; struct formatter_item { LIST_HEADER (struct formatter_item) enum formatter_item_type type; ///< Type of this item int color; ///< Color int attribute; ///< Attribute ID char *text; ///< Either text or an attribute string }; static struct formatter_item * formatter_item_new (void) { struct formatter_item *self = xcalloc (1, sizeof *self); return self; } static void formatter_item_destroy (struct formatter_item *self) { free (self->text); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct formatter { struct app_context *ctx; ///< Application context bool ignore_new_attributes; ///< Whether to ignore new attributes struct formatter_item *items; ///< Items struct formatter_item *items_tail; ///< Tail of items }; static void formatter_init (struct formatter *self, struct app_context *ctx) { memset (self, 0, sizeof *self); self->ctx = ctx; } static void formatter_free (struct formatter *self) { LIST_FOR_EACH (struct formatter_item, iter, self->items) formatter_item_destroy (iter); } static struct formatter_item * formatter_add_blank (struct formatter *self) { struct formatter_item *item = formatter_item_new (); LIST_APPEND_WITH_TAIL (self->items, self->items_tail, item); return item; } static void formatter_add_text (struct formatter *self, const char *text) { struct formatter_item *item = formatter_add_blank (self); item->type = FORMATTER_ITEM_TEXT; item->text = xstrdup (text); } static void formatter_add_reset (struct formatter *self) { if (self->ignore_new_attributes) return; struct formatter_item *item = formatter_add_blank (self); item->type = FORMATTER_ITEM_ATTR; item->attribute = ATTR_RESET; } static void formatter_add_attr (struct formatter *self, int attr_id) { if (self->ignore_new_attributes) return; struct formatter_item *item = formatter_add_blank (self); item->type = FORMATTER_ITEM_ATTR; item->attribute = attr_id; } static void formatter_add_fg_color (struct formatter *self, int color) { if (self->ignore_new_attributes) return; struct formatter_item *item = formatter_add_blank (self); item->type = FORMATTER_ITEM_FG_COLOR; item->color = color; } static void formatter_add_bg_color (struct formatter *self, int color) { if (self->ignore_new_attributes) return; struct formatter_item *item = formatter_add_blank (self); item->type = FORMATTER_ITEM_BG_COLOR; item->color = color; } static const char * formatter_parse_field (struct formatter *self, const char *field, struct str *buf, va_list *ap) { size_t width = 0; bool zero_padded = false; int c; restart: switch ((c = *field++)) { char *s; // We can push boring text content to the caller's buffer // and let it flush the buffer only when it's actually needed case 's': s = va_arg (*ap, char *); for (size_t len = strlen (s); len < width; len++) str_append_c (buf, ' '); str_append (buf, s); break; case 'd': s = xstrdup_printf ("%d", va_arg (*ap, int)); for (size_t len = strlen (s); len < width; len++) str_append_c (buf, " 0"[zero_padded]); str_append (buf, s); free (s); break; case 'a': formatter_add_attr (self, va_arg (*ap, int)); break; case 'c': formatter_add_fg_color (self, va_arg (*ap, int)); break; case 'C': formatter_add_bg_color (self, va_arg (*ap, int)); break; case 'r': formatter_add_reset (self); break; default: if (c == '0' && !zero_padded) zero_padded = true; else if (isdigit_ascii (c)) width = width * 10 + (c - '0'); else if (c) hard_assert (!"unexpected format specifier"); else hard_assert (!"unexpected end of format string"); goto restart; } return field; } static void formatter_add (struct formatter *self, const char *format, ...) { struct str buf; str_init (&buf); va_list ap; va_start (ap, format); while (*format) { if (*format != '#' || *++format == '#') { str_append_c (&buf, *format++); continue; } if (buf.len) { formatter_add_text (self, buf.str); str_reset (&buf); } format = formatter_parse_field (self, format, &buf, &ap); } if (buf.len) formatter_add_text (self, buf.str); str_free (&buf); va_end (ap); } static void formatter_flush (struct formatter *self, FILE *stream) { terminal_printer_fn printer = get_attribute_printer (stream); if (!printer) { LIST_FOR_EACH (struct formatter_item, iter, self->items) if (iter->type == FORMATTER_ITEM_TEXT) fputs (iter->text, stream); return; } const char *attr_reset = self->ctx->attrs[ATTR_RESET]; tputs (attr_reset, 1, printer); bool is_attributed = false; LIST_FOR_EACH (struct formatter_item, iter, self->items) { switch (iter->type) { char *term; case FORMATTER_ITEM_TEXT: term = iconv_xstrdup (self->ctx->term_from_utf8, iter->text, -1, NULL); fputs (term, stream); free (term); break; case FORMATTER_ITEM_ATTR: if (is_attributed) { tputs (attr_reset, 1, printer); is_attributed = false; } if (iter->attribute != ATTR_RESET) { tputs (self->ctx->attrs[iter->attribute], 1, printer); is_attributed = true; } break; case FORMATTER_ITEM_FG_COLOR: tputs (g_terminal.color_set_fg[iter->color], 1, printer); is_attributed = true; break; case FORMATTER_ITEM_BG_COLOR: tputs (g_terminal.color_set_bg[iter->color], 1, printer); is_attributed = true; break; } } if (is_attributed) tputs (attr_reset, 1, printer); } // --- Buffers ----------------------------------------------------------------- static void buffer_update_time (struct app_context *ctx, time_t now) { struct tm last, current; if (!localtime_r (&ctx->last_displayed_msg_time, &last) || !localtime_r (&now, ¤t)) { // Strange but nonfatal print_error ("%s: %s", "localtime_r", strerror (errno)); return; } ctx->last_displayed_msg_time = now; if (last.tm_year == current.tm_year && last.tm_mon == current.tm_mon && last.tm_mday == current.tm_mday) return; char buf[32] = ""; if (soft_assert (strftime (buf, sizeof buf, "%F", ¤t))) print_status ("%s", buf); // Else the buffer was too small, which is pretty weird } static void buffer_line_display (struct app_context *ctx, struct buffer_line *line, bool is_external) { // Normal timestamps don't include the date, this way the user won't be // confused as to when an event has happened buffer_update_time (ctx, line->when); struct buffer_line_args *a = &line->args; char *nick = NULL; const char *userhost = NULL; int nick_color = -1; int object_color = -1; if (a->who) { nick = irc_cut_nickname (a->who); userhost = irc_find_userhost (a->who); nick_color = str_map_hash (nick, strlen (nick)) % 8; } if (a->object) object_color = str_map_hash (a->object, strlen (a->object)) % 8; struct formatter f; formatter_init (&f, ctx); struct tm current; if (!localtime_r (&line->when, ¤t)) print_error ("%s: %s", "localtime_r", strerror (errno)); else formatter_add (&f, "#a#02d:#02d:#02d#r ", ATTR_TIMESTAMP, current.tm_hour, current.tm_min, current.tm_sec); // Ignore all formatting for messages coming from other buffers, that is // either from the global or server buffer. Instead print them in grey. if (is_external) { formatter_add (&f, "#a", ATTR_EXTERNAL); f.ignore_new_attributes = true; } // TODO: try to decode as much as possible using mIRC formatting; // could either add a #m format specifier, or write a separate function // to translate the formatting into formatter API calls switch (line->type) { case BUFFER_LINE_PRIVMSG: if (line->flags & BUFFER_LINE_HIGHLIGHT) formatter_add (&f, "#a<#s>#r #s", ATTR_HIGHLIGHT, nick, a->text); else formatter_add (&f, "<#c#s#r> #s", nick_color, nick, a->text); break; case BUFFER_LINE_ACTION: if (line->flags & BUFFER_LINE_HIGHLIGHT) formatter_add (&f, " #a*#r ", ATTR_HIGHLIGHT); else formatter_add (&f, " #a*#r ", ATTR_ACTION); formatter_add (&f, "#c#s#r #s", nick_color, nick, a->text); break; case BUFFER_LINE_NOTICE: formatter_add (&f, " - "); if (line->flags & BUFFER_LINE_HIGHLIGHT) formatter_add (&f, "#a#s(#s)#r: #s", ATTR_HIGHLIGHT, "Notice", nick, a->text); else formatter_add (&f, "#s(#c#s#r): #s", "Notice", nick_color, nick, a->text); break; case BUFFER_LINE_JOIN: formatter_add (&f, "#a-->#r ", ATTR_JOIN); formatter_add (&f, "#c#s#r (#a#s#r) #a#s#r #s", nick_color, nick, ATTR_USERHOST, userhost, ATTR_JOIN, "has joined", a->object); break; case BUFFER_LINE_PART: formatter_add (&f, "#a<--#r ", ATTR_PART); formatter_add (&f, "#c#s#r (#a#s#r) #a#s#r #s", nick_color, nick, ATTR_USERHOST, userhost, ATTR_PART, "has left", a->object); if (a->reason) formatter_add (&f, " (#s)", a->reason); break; case BUFFER_LINE_KICK: formatter_add (&f, "#a<--#r ", ATTR_PART); formatter_add (&f, "#c#s#r (#a#s#r) #a#s#r #c#s#r", nick_color, nick, ATTR_USERHOST, userhost, ATTR_PART, "has kicked", object_color, a->object); if (a->reason) formatter_add (&f, " (#s)", a->reason); break; case BUFFER_LINE_NICK: formatter_add (&f, " - "); if (a->who) formatter_add (&f, "#c#s#r #s #c#s#r", nick_color, nick, "is now known as", object_color, a->object); else formatter_add (&f, "#s #s", "You are now known as", a->object); break; case BUFFER_LINE_TOPIC: formatter_add (&f, " - "); formatter_add (&f, "#c#s#r #s \"#s\"", nick_color, nick, "has changed the topic to", a->text); break; case BUFFER_LINE_QUIT: formatter_add (&f, "#a<--#r ", ATTR_PART); formatter_add (&f, "#c#s#r (#a%s#r) #a#s#r", nick_color, nick, ATTR_USERHOST, userhost, ATTR_PART, "has quit"); if (a->reason) formatter_add (&f, " (#s)", a->reason); break; case BUFFER_LINE_STATUS: formatter_add (&f, " - "); formatter_add (&f, "#s", a->text); break; case BUFFER_LINE_ERROR: formatter_add (&f, "#a=!=#r ", ATTR_ERROR); formatter_add (&f, "#s", a->text); } free (nick); input_hide (&ctx->input); // TODO: write the line to a log file; note that the global and server // buffers musn't collide with filenames formatter_add (&f, "\n"); formatter_flush (&f, stdout); formatter_free (&f); input_show (&ctx->input); } static void buffer_send_internal (struct app_context *ctx, struct buffer *buffer, enum buffer_line_type type, int flags, struct buffer_line_args a) { struct buffer_line *line = buffer_line_new (); line->type = type; line->flags = flags; line->when = time (NULL); line->args = a; LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line); buffer->lines_count++; if (buffer == ctx->current_buffer) { buffer_line_display (ctx, line, false); return; } bool can_leak = false; if ((buffer == ctx->global_buffer) || (ctx->current_buffer->type == BUFFER_GLOBAL && buffer->type == BUFFER_SERVER) || (ctx->current_buffer->type != BUFFER_GLOBAL && buffer == ctx->current_buffer->server->buffer)) can_leak = true; if (!ctx->isolate_buffers && can_leak) buffer_line_display (ctx, line, true); else { buffer->unseen_messages_count++; refresh_prompt (ctx); } } #define buffer_send(ctx, buffer, type, flags, ...) \ buffer_send_internal ((ctx), (buffer), (type), (flags), \ (struct buffer_line_args) { __VA_ARGS__ }) #define buffer_send_status(ctx, buffer, ...) \ buffer_send (ctx, buffer, BUFFER_LINE_STATUS, 0, \ .text = xstrdup_printf (__VA_ARGS__)) #define buffer_send_error(ctx, buffer, ...) \ buffer_send (ctx, buffer, BUFFER_LINE_ERROR, 0, \ .text = xstrdup_printf (__VA_ARGS__)) static struct buffer * buffer_by_name (struct app_context *ctx, const char *name) { return str_map_find (&ctx->buffers_by_name, name); } static void buffer_add (struct app_context *ctx, struct buffer *buffer) { hard_assert (!buffer_by_name (ctx, buffer->name)); str_map_set (&ctx->buffers_by_name, buffer->name, buffer); LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer); // In theory this can't cause changes in the prompt refresh_prompt (ctx); } static void buffer_remove (struct app_context *ctx, struct buffer *buffer) { hard_assert (buffer != ctx->current_buffer); // TODO: part from the channel if needed input_destroy_buffer (&ctx->input, buffer->input_data); buffer->input_data = NULL; // And make sure to unlink the buffer from "irc_buffer_map" struct server *s = buffer->server; if (buffer->channel) str_map_set (&s->irc_buffer_map, buffer->channel->name, NULL); if (buffer->user) str_map_set (&s->irc_buffer_map, buffer->user->nickname, NULL); str_map_set (&ctx->buffers_by_name, buffer->name, NULL); LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer); buffer_destroy (buffer); if (buffer == ctx->last_buffer) ctx->last_buffer = NULL; // It's not a good idea to remove these buffers, but it's even a worse // one to leave the pointers point to invalid memory if (buffer == ctx->global_buffer) ctx->global_buffer = NULL; if (buffer == ctx->server.buffer) ctx->server.buffer = NULL; refresh_prompt (ctx); } static void buffer_activate (struct app_context *ctx, struct buffer *buffer) { if (ctx->current_buffer == buffer) return; print_status ("%s", buffer->name); // That is, minus the buffer switch line and the readline prompt int to_display = MAX (10, g_terminal.lines - 2); struct buffer_line *line = buffer->lines_tail; while (line && line->prev && --to_display > 0) line = line->prev; // Once we've found where we want to start with the backlog, print it for (; line; line = line->next) buffer_line_display (ctx, line, false); buffer->unseen_messages_count = 0; input_switch_buffer (&ctx->input, buffer->input_data); // Now at last we can switch the pointers ctx->last_buffer = ctx->current_buffer; ctx->current_buffer = buffer; refresh_prompt (ctx); } static void buffer_merge (struct app_context *ctx, struct buffer *buffer, struct buffer *merged) { // TODO: try to merge the buffers as best as we can } static void buffer_rename (struct app_context *ctx, struct buffer *buffer, const char *new_name) { hard_assert (buffer->type == BUFFER_PM); struct buffer *collision = str_map_find (&buffer->server->irc_buffer_map, new_name); if (collision) { // TODO: use full weechat-style buffer names // to prevent name collisions with the global buffer hard_assert (collision->type == BUFFER_PM); // When there's a collision, there's not much else we can do // other than somehow trying to merge them buffer_merge (ctx, collision, buffer); // TODO: log a status message about the merge if (ctx->current_buffer == buffer) buffer_activate (ctx, collision); buffer_remove (ctx, buffer); } else { // Otherwise we just rename the buffer and that's it str_map_set (&ctx->buffers_by_name, buffer->name, NULL); str_map_set (&ctx->buffers_by_name, new_name, buffer); free (buffer->name); buffer->name = xstrdup (new_name); // We might have renamed the current buffer refresh_prompt (ctx); } } static struct buffer * buffer_at_index (struct app_context *ctx, int n) { int i = 0; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) if (++i == n) return iter; return NULL; } static struct buffer * buffer_next (struct app_context *ctx, int count) { struct buffer *new_buffer = ctx->current_buffer; while (count-- > 0) if (!(new_buffer = new_buffer->next)) new_buffer = ctx->buffers; return new_buffer; } static struct buffer * buffer_previous (struct app_context *ctx, int count) { struct buffer *new_buffer = ctx->current_buffer; while (count-- > 0) if (!(new_buffer = new_buffer->prev)) new_buffer = ctx->buffers_tail; return new_buffer; } static bool buffer_goto (struct app_context *ctx, int n) { struct buffer *buffer = buffer_at_index (ctx, n); if (!buffer) return false; buffer_activate (ctx, buffer); return true; } static int buffer_get_index (struct app_context *ctx, struct buffer *buffer) { int index = 1; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) { if (iter == buffer) return index; index++; } return -1; } static void init_buffers (struct app_context *ctx) { // At the moment we have only two global everpresent buffers struct buffer *global = ctx->global_buffer = buffer_new (); struct buffer *server = ctx->server.buffer = buffer_new (); global->type = BUFFER_GLOBAL; global->name = xstrdup (PROGRAM_NAME); server->type = BUFFER_SERVER; server->name = xstrdup ("server"); server->server = &ctx->server; LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, global); LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, server); } // --- Users, channels --------------------------------------------------------- static void irc_user_on_destroy (void *object, void *user_data) { struct user *user = object; struct server *s = user_data; str_map_set (&s->irc_users, user->nickname, NULL); } static struct user * irc_make_user (struct server *s, char *nickname) { hard_assert (!str_map_find (&s->irc_users, nickname)); struct user *user = user_new (); user->on_destroy = irc_user_on_destroy; user->user_data = s; user->nickname = nickname; str_map_set (&s->irc_users, user->nickname, user); return user; } static struct buffer * irc_get_or_make_user_buffer (struct server *s, const char *nickname) { struct buffer *buffer = str_map_find (&s->irc_buffer_map, nickname); if (buffer) return buffer; struct user *user = str_map_find (&s->irc_users, nickname); if (!user) user = irc_make_user (s, xstrdup (nickname)); else user = user_ref (user); // Open a new buffer for the user buffer = buffer_new (); buffer->type = BUFFER_PM; buffer->name = xstrdup (nickname); buffer->server = s; buffer->user = user; LIST_APPEND_WITH_TAIL (s->ctx->buffers, s->ctx->buffers_tail, buffer); str_map_set (&s->irc_buffer_map, user->nickname, buffer); return buffer; } static void irc_channel_unlink_user (struct channel *channel, struct channel_user *channel_user) { // First destroy the user's weak references to the channel struct user *user = channel_user->user; LIST_FOR_EACH (struct user_channel, iter, user->channels) if (iter->channel == channel) { LIST_UNLINK (user->channels, iter); user_channel_destroy (iter); } // Then just unlink the user from the channel LIST_UNLINK (channel->users, channel_user); channel_user_destroy (channel_user); } static void irc_channel_on_destroy (void *object, void *user_data) { struct channel *channel = object; struct server *s = user_data; LIST_FOR_EACH (struct channel_user, iter, channel->users) irc_channel_unlink_user (channel, iter); str_map_set (&s->irc_channels, channel->name, NULL); } static struct channel * irc_make_channel (struct server *s, char *name) { hard_assert (!str_map_find (&s->irc_channels, name)); struct channel *channel = channel_new (); channel->on_destroy = irc_channel_on_destroy; channel->user_data = s; channel->name = name; channel->mode = xstrdup (""); channel->topic = NULL; str_map_set (&s->irc_channels, channel->name, channel); return channel; } static void irc_remove_user_from_channel (struct user *user, struct channel *channel) { LIST_FOR_EACH (struct channel_user, iter, channel->users) if (iter->user == user) irc_channel_unlink_user (channel, iter); } // --- Supporting code --------------------------------------------------------- static bool irc_connect (struct server *s, bool *should_retry, struct error **); static void irc_cancel_timers (struct server *s); static void on_irc_reconnect_timeout (void *user_data); static char * irc_cut_nickname (const char *prefix) { return xstrndup (prefix, strcspn (prefix, "!@")); } static const char * irc_find_userhost (const char *prefix) { const char *p = strchr (prefix, '!'); return p ? p + 1 : NULL; } static bool irc_is_this_us (struct server *s, const char *prefix) { char *nick = irc_cut_nickname (prefix); bool result = !irc_strcmp (nick, s->irc_user->nickname); free (nick); return result; } static bool irc_is_channel (struct server *s, const char *ident) { (void) s; // TODO: parse prefixes from server features return *ident && !!strchr ("#&+!", *ident); } static void irc_shutdown (struct server *s) { // TODO: set a timer after which we cut the connection? // Generally non-critical if (s->ssl) soft_assert (SSL_shutdown (s->ssl) != -1); else soft_assert (shutdown (s->irc_fd, SHUT_WR) == 0); } static void try_finish_quit (struct app_context *ctx) { // TODO: multiserver if (ctx->quitting && ctx->server.irc_fd == -1) ctx->polling = false; } static void initiate_quit (struct app_context *ctx) { // Destroy the user interface input_stop (&ctx->input); buffer_send_status (ctx, ctx->global_buffer, "Shutting down"); // Initiate a connection close // TODO: multiserver struct server *s = &ctx->server; if (s->irc_fd != -1) // XXX: when we go async, we'll have to flush output buffers first irc_shutdown (s); ctx->quitting = true; try_finish_quit (ctx); } // As of 2015, everything should be in UTF-8. And if it's not, we'll decode it // as ISO Latin 1. This function should not be called on the whole message. static char * irc_to_utf8 (struct app_context *ctx, const char *text) { size_t len = strlen (text) + 1; if (utf8_validate (text, len)) return xstrdup (text); return iconv_xstrdup (ctx->latin1_to_utf8, (char *) text, len, NULL); } // This function is used to output debugging IRC traffic to the terminal. // It's far from ideal, as any non-UTF-8 text degrades the entire line to // ISO Latin 1. But it should work good enough most of the time. static char * irc_to_term (struct app_context *ctx, const char *text) { char *utf8 = irc_to_utf8 (ctx, text); char *term = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL); free (utf8); return term; } static bool irc_send (struct server *s, const char *format, ...) ATTRIBUTE_PRINTF (2, 3); static bool irc_send (struct server *s, const char *format, ...) { if (!soft_assert (s->irc_fd != -1)) { print_debug ("tried sending a message to a dead server connection"); return false; } va_list ap; va_start (ap, format); struct str str; str_init (&str); str_append_vprintf (&str, format, ap); va_end (ap); if (g_debug_mode) { input_hide (&s->ctx->input); char *term = irc_to_term (s->ctx, str.str); fprintf (stderr, "[IRC] <== \"%s\"\n", term); free (term); input_show (&s->ctx->input); } str_append (&str, "\r\n"); bool result = true; if (s->ssl) { // TODO: call SSL_get_error() to detect if a clean shutdown has occured if (SSL_write (s->ssl, str.str, str.len) != (int) str.len) { LOG_FUNC_FAILURE ("SSL_write", ERR_error_string (ERR_get_error (), NULL)); result = false; } } else if (write (s->irc_fd, str.str, str.len) != (ssize_t) str.len) { LOG_LIBC_FAILURE ("write"); result = false; } str_free (&str); return result; } static bool irc_initialize_ssl_ctx (struct server *s, struct error **e) { // XXX: maybe we should call SSL_CTX_set_options() for some workarounds bool verify = get_config_boolean (s->ctx, "server.ssl_verify"); if (!verify) SSL_CTX_set_verify (s->ssl_ctx, SSL_VERIFY_NONE, NULL); const char *ca_file = get_config_string (s->ctx, "server.ca_file"); const char *ca_path = get_config_string (s->ctx, "server.ca_path"); struct error *error = NULL; if (ca_file || ca_path) { if (SSL_CTX_load_verify_locations (s->ssl_ctx, ca_file, ca_path)) return true; error_set (&error, "%s: %s", "Failed to set locations for the CA certificate bundle", ERR_reason_error_string (ERR_get_error ())); goto ca_error; } if (!SSL_CTX_set_default_verify_paths (s->ssl_ctx)) { error_set (&error, "%s: %s", "Couldn't load the default CA certificate bundle", ERR_reason_error_string (ERR_get_error ())); goto ca_error; } return true; ca_error: if (verify) { error_propagate (e, error); return false; } // Only inform the user if we're not actually verifying buffer_send_error (s->ctx, s->buffer, "%s", error->message); error_free (error); return true; } static bool irc_initialize_ssl (struct server *s, struct error **e) { const char *error_info = NULL; s->ssl_ctx = SSL_CTX_new (SSLv23_client_method ()); if (!s->ssl_ctx) goto error_ssl_1; if (!irc_initialize_ssl_ctx (s, e)) goto error_ssl_2; s->ssl = SSL_new (s->ssl_ctx); if (!s->ssl) goto error_ssl_2; const char *ssl_cert = get_config_string (s->ctx, "server.ssl_cert"); if (ssl_cert) { char *path = resolve_config_filename (ssl_cert); if (!path) buffer_send_error (s->ctx, s->ctx->global_buffer, "%s: %s", "Cannot open file", ssl_cert); // XXX: perhaps we should read the file ourselves for better messages else if (!SSL_use_certificate_file (s->ssl, path, SSL_FILETYPE_PEM) || !SSL_use_PrivateKey_file (s->ssl, path, SSL_FILETYPE_PEM)) buffer_send_error (s->ctx, s->ctx->global_buffer, "%s: %s", "Setting the SSL client certificate failed", ERR_error_string (ERR_get_error (), NULL)); free (path); } SSL_set_connect_state (s->ssl); if (!SSL_set_fd (s->ssl, s->irc_fd)) goto error_ssl_3; // Avoid SSL_write() returning SSL_ERROR_WANT_READ SSL_set_mode (s->ssl, SSL_MODE_AUTO_RETRY); switch (xssl_get_error (s->ssl, SSL_connect (s->ssl), &error_info)) { case SSL_ERROR_NONE: return true; case SSL_ERROR_ZERO_RETURN: error_info = "server closed the connection"; default: break; } error_ssl_3: SSL_free (s->ssl); s->ssl = NULL; error_ssl_2: SSL_CTX_free (s->ssl_ctx); s->ssl_ctx = NULL; error_ssl_1: // XXX: these error strings are really nasty; also there could be // multiple errors on the OpenSSL stack. if (!error_info) error_info = ERR_error_string (ERR_get_error (), NULL); error_set (e, "%s: %s", "could not initialize SSL", error_info); return false; } static bool irc_establish_connection (struct server *s, const char *host, const char *port, struct error **e) { struct addrinfo gai_hints, *gai_result, *gai_iter; memset (&gai_hints, 0, sizeof gai_hints); gai_hints.ai_socktype = SOCK_STREAM; int err = getaddrinfo (host, port, &gai_hints, &gai_result); if (err) { error_set (e, "%s: %s: %s", "connection failed", "getaddrinfo", gai_strerror (err)); return false; } int sockfd; for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next) { sockfd = socket (gai_iter->ai_family, gai_iter->ai_socktype, gai_iter->ai_protocol); if (sockfd == -1) continue; set_cloexec (sockfd); int yes = 1; soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof yes) != -1); const char *real_host = host; // Let's try to resolve the address back into a real hostname; // we don't really need this, so we can let it quietly fail char buf[NI_MAXHOST]; err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen, buf, sizeof buf, NULL, 0, NI_NUMERICHOST); if (err) LOG_FUNC_FAILURE ("getnameinfo", gai_strerror (err)); else real_host = buf; char *address = format_host_port_pair (real_host, port); buffer_send_status (s->ctx, s->buffer, "Connecting to %s...", address); free (address); if (!connect (sockfd, gai_iter->ai_addr, gai_iter->ai_addrlen)) break; xclose (sockfd); } freeaddrinfo (gai_result); if (!gai_iter) { error_set (e, "connection failed"); return false; } s->irc_fd = sockfd; return true; } // --- More readline funky stuff ----------------------------------------------- static char * make_unseen_prefix (struct app_context *ctx) { struct str active_buffers; str_init (&active_buffers); size_t i = 0; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) { i++; if (!iter->unseen_messages_count) continue; if (active_buffers.len) str_append_c (&active_buffers, ','); str_append_printf (&active_buffers, "%zu", i); } if (active_buffers.len) return str_steal (&active_buffers); str_free (&active_buffers); return NULL; } static void make_prompt (struct app_context *ctx, struct str *output) { struct buffer *buffer = ctx->current_buffer; if (!buffer) return; str_append_c (output, '['); char *unseen_prefix = make_unseen_prefix (ctx); if (unseen_prefix) str_append_printf (output, "(%s) ", unseen_prefix); free (unseen_prefix); str_append_printf (output, "%d:%s", buffer_get_index (ctx, buffer), buffer->name); if (buffer->type == BUFFER_CHANNEL && *buffer->channel->mode) str_append_printf (output, "(%s)", buffer->channel->mode); if (buffer != ctx->global_buffer) { struct server *s = buffer->server; str_append_c (output, ' '); if (s->irc_fd == -1) str_append (output, "(disconnected)"); else { str_append (output, s->irc_user->nickname); if (*s->irc_user_mode) str_append_printf (output, "(%s)", s->irc_user_mode); } } str_append_c (output, ']'); } static void refresh_prompt (struct app_context *ctx) { bool have_attributes = !!get_attribute_printer (stdout); struct str prompt; str_init (&prompt); make_prompt (ctx, &prompt); str_append_c (&prompt, ' '); if (have_attributes) { // XXX: to be completely correct, we should use tputs, but we cannot input_set_prompt (&ctx->input, xstrdup_printf ("%c%s%c%s%c%s%c", INPUT_START_IGNORE, ctx->attrs[ATTR_PROMPT], INPUT_END_IGNORE, prompt.str, INPUT_START_IGNORE, ctx->attrs[ATTR_RESET], INPUT_END_IGNORE)); } else input_set_prompt (&ctx->input, xstrdup (prompt.str)); str_free (&prompt); } // --- Input handling ---------------------------------------------------------- // TODO: we will need a proper mode parser; to be shared with kike // TODO: we alse definitely need to parse server capability messages static struct buffer * irc_get_buffer_for_message (struct server *s, const struct irc_message *msg, const char *target) { struct buffer *buffer = str_map_find (&s->irc_buffer_map, target); if (irc_is_channel (s, target)) { struct channel *channel = str_map_find (&s->irc_channels, target); hard_assert ((channel && buffer) || (channel && !buffer) || (!channel && !buffer)); // This is weird if (!channel) return NULL; } else if (!buffer) { // Implying that the target is us // Don't make user buffers for servers (they can send NOTICEs) if (!irc_find_userhost (msg->prefix)) return s->buffer; char *nickname = irc_cut_nickname (msg->prefix); buffer = irc_get_or_make_user_buffer (s, nickname); free (nickname); } return buffer; } static bool irc_is_highlight (struct server *s, const char *message) { // Well, this is rather crude but it should make most users happy. // Ideally we could do this at least in proper Unicode. char *copy = xstrdup (message); for (char *p = copy; *p; p++) *p = irc_tolower (*p); char *nick = xstrdup (s->irc_user->nickname); for (char *p = nick; *p; p++) *p = irc_tolower (*p); // Special characters allowed in nicknames by RFC 2812: []\`_^{|} and - // Also excluded from the ASCII: common user channel prefixes: +%@&~ const char *delimiters = ",.;:!?()<>/=#$* \t\r\n\v\f\"'"; bool result = false; char *save = NULL; for (char *token = strtok_r (copy, delimiters, &save); token; token = strtok_r (NULL, delimiters, &save)) if (!strcmp (token, nick)) { result = true; break; } free (copy); free (nick); return result; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void irc_handle_join (struct server *s, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 1) return; const char *channel_name = msg->params.vector[0]; if (!irc_is_channel (s, channel_name)) return; struct channel *channel = str_map_find (&s->irc_channels, channel_name); struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name); hard_assert ((channel && buffer) || (channel && !buffer) || (!channel && !buffer)); // We've joined a new channel if (!channel && irc_is_this_us (s, msg->prefix)) { buffer = buffer_new (); buffer->type = BUFFER_CHANNEL; buffer->name = xstrdup (channel_name); buffer->server = s; buffer->channel = channel = irc_make_channel (s, xstrdup (channel_name)); LIST_APPEND_WITH_TAIL (s->ctx->buffers, s->ctx->buffers_tail, buffer); str_map_set (&s->irc_buffer_map, channel->name, buffer); buffer_activate (s->ctx, buffer); } // This is weird, ignoring if (!channel) return; // Get or make a user object char *nickname = irc_cut_nickname (msg->prefix); struct user *user = str_map_find (&s->irc_users, nickname); if (!user) user = irc_make_user (s, nickname); else { user = user_ref (user); free (nickname); } // Link the user with the channel struct user_channel *user_channel = user_channel_new (); user_channel->channel = channel; LIST_PREPEND (user->channels, user_channel); struct channel_user *channel_user = channel_user_new (); channel_user->user = user; channel_user->modes = xstrdup (""); LIST_PREPEND (channel->users, channel_user); // Finally log the message if (buffer) { buffer_send (s->ctx, buffer, BUFFER_LINE_JOIN, 0, .who = irc_to_utf8 (s->ctx, msg->prefix), .object = irc_to_utf8 (s->ctx, channel_name)); } } static void irc_handle_kick (struct server *s, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 2) return; const char *channel_name = msg->params.vector[0]; const char *target = msg->params.vector[1]; if (!irc_is_channel (s, channel_name) || irc_is_channel (s, target)) return; const char *message = ""; if (msg->params.len > 2) message = msg->params.vector[2]; struct user *user = str_map_find (&s->irc_users, target); struct channel *channel = str_map_find (&s->irc_channels, channel_name); struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name); hard_assert ((channel && buffer) || (channel && !buffer) || (!channel && !buffer)); // It would be is weird for this to be false if (user && channel) irc_remove_user_from_channel (user, channel); if (buffer) { buffer_send (s->ctx, buffer, BUFFER_LINE_KICK, 0, .who = irc_to_utf8 (s->ctx, msg->prefix), .object = irc_to_utf8 (s->ctx, target), .reason = irc_to_utf8 (s->ctx, message)); } } static void irc_handle_mode (struct server *s, const struct irc_message *msg) { // TODO: parse the mode change and apply it // TODO: log a message } static void irc_handle_nick (struct server *s, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 1) return; const char *new_nickname = msg->params.vector[0]; char *nickname = irc_cut_nickname (msg->prefix); struct user *user = str_map_find (&s->irc_users, nickname); free (nickname); if (!user) return; // What the fuck // TODO: probably log a message and force a reconnect if (str_map_find (&s->irc_users, new_nickname)) return; // Log a message in any PM buffer and rename it; // we may even have one for ourselves struct buffer *pm_buffer = str_map_find (&s->irc_buffer_map, user->nickname); if (pm_buffer) { str_map_set (&s->irc_buffer_map, new_nickname, pm_buffer); str_map_set (&s->irc_buffer_map, user->nickname, NULL); char *who = irc_is_this_us (s, msg->prefix) ? irc_to_utf8 (s->ctx, msg->prefix) : NULL; buffer_send (s->ctx, pm_buffer, BUFFER_LINE_NICK, 0, .who = who, .object = irc_to_utf8 (s->ctx, new_nickname)); // TODO: use a full weechat-style buffer name here buffer_rename (s->ctx, pm_buffer, new_nickname); } if (irc_is_this_us (s, msg->prefix)) { // Log a message in all open buffers on this server struct str_map_iter iter; str_map_iter_init (&iter, &s->irc_buffer_map); struct buffer *buffer; while ((buffer = str_map_iter_next (&iter))) { // We've already done that if (buffer == pm_buffer) continue; buffer_send (s->ctx, buffer, BUFFER_LINE_NICK, 0, .object = irc_to_utf8 (s->ctx, new_nickname)); } } else { // Log a message in all channels the user is in LIST_FOR_EACH (struct user_channel, iter, user->channels) { struct buffer *buffer = str_map_find (&s->irc_buffer_map, iter->channel->name); hard_assert (buffer != NULL); buffer_send (s->ctx, buffer, BUFFER_LINE_NICK, 0, .who = irc_to_utf8 (s->ctx, msg->prefix), .object = irc_to_utf8 (s->ctx, new_nickname)); } } // Finally rename the user str_map_set (&s->irc_users, new_nickname, user_ref (user)); str_map_set (&s->irc_users, user->nickname, NULL); free (user->nickname); user->nickname = xstrdup (new_nickname); // We might have renamed ourselves refresh_prompt (s->ctx); } static void irc_handle_ctcp_reply (struct server *s, const struct irc_message *msg, struct ctcp_chunk *chunk) { char *nickname = irc_cut_nickname (msg->prefix); char *nickname_utf8 = irc_to_utf8 (s->ctx, nickname); char *tag_utf8 = irc_to_utf8 (s->ctx, chunk->tag.str); char *text_utf8 = irc_to_utf8 (s->ctx, chunk->text.str); buffer_send_status (s->ctx, s->buffer, "CTCP reply from %s: %s %s", nickname_utf8, tag_utf8, text_utf8); free (nickname); free (nickname_utf8); free (tag_utf8); free (text_utf8); } static void irc_handle_notice_text (struct server *s, const struct irc_message *msg, struct str *text) { const char *target = msg->params.vector[0]; struct buffer *buffer = irc_get_buffer_for_message (s, msg, target); if (buffer) { // TODO: some more obvious indication of highlights int flags = irc_is_highlight (s, text->str) ? BUFFER_LINE_HIGHLIGHT : 0; buffer_send (s->ctx, buffer, BUFFER_LINE_NOTICE, flags, .who = irc_to_utf8 (s->ctx, msg->prefix), .text = irc_to_utf8 (s->ctx, text->str)); } } static void irc_handle_notice (struct server *s, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 2) return; // This ignores empty messages which we should never receive anyway struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]); LIST_FOR_EACH (struct ctcp_chunk, iter, chunks) if (!iter->is_extended) irc_handle_notice_text (s, msg, &iter->text); else irc_handle_ctcp_reply (s, msg, iter); ctcp_destroy (chunks); } static void irc_handle_part (struct server *s, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 1) return; const char *channel_name = msg->params.vector[0]; if (!irc_is_channel (s, channel_name)) return; const char *message = ""; if (msg->params.len > 1) message = msg->params.vector[1]; char *nickname = irc_cut_nickname (msg->prefix); struct user *user = str_map_find (&s->irc_users, nickname); free (nickname); struct channel *channel = str_map_find (&s->irc_channels, channel_name); struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name); hard_assert ((channel && buffer) || (channel && !buffer) || (!channel && !buffer)); // It would be is weird for this to be false if (user && channel) irc_remove_user_from_channel (user, channel); if (buffer) { buffer_send (s->ctx, buffer, BUFFER_LINE_PART, 0, .who = irc_to_utf8 (s->ctx, msg->prefix), .object = irc_to_utf8 (s->ctx, channel_name), .reason = irc_to_utf8 (s->ctx, message)); } } static void irc_handle_ping (struct server *s, const struct irc_message *msg) { if (msg->params.len) irc_send (s, "PONG :%s", msg->params.vector[0]); else irc_send (s, "PONG"); } static char * ctime_now (char buf[26]) { struct tm tm_; time_t now = time (NULL); if (!asctime_r (localtime_r (&now, &tm_), buf)) return NULL; // Annoying thing *strchr (buf, '\n') = '\0'; return buf; } static void irc_send_ctcp_reply (struct server *s, const char *recipient, const char *format, ...) ATTRIBUTE_PRINTF (3, 4); static void irc_send_ctcp_reply (struct server *s, const char *recipient, const char *format, ...) { struct str m; str_init (&m); va_list ap; va_start (ap, format); str_append_vprintf (&m, format, ap); va_end (ap); irc_send (s, "NOTICE %s :\x01%s\x01", recipient, m.str); char *text_utf8 = irc_to_utf8 (s->ctx, m.str); char *recipient_utf8 = irc_to_utf8 (s->ctx, recipient); str_free (&m); buffer_send_status (s->ctx, s->buffer, "CTCP reply to %s: %s", recipient_utf8, text_utf8); free (text_utf8); free (recipient_utf8); } static void irc_handle_ctcp_request (struct server *s, const struct irc_message *msg, struct ctcp_chunk *chunk) { char *nickname = irc_cut_nickname (msg->prefix); char *nickname_utf8 = irc_to_utf8 (s->ctx, nickname); char *tag_utf8 = irc_to_utf8 (s->ctx, chunk->tag.str); buffer_send_status (s->ctx, s->buffer, "CTCP requested by %s: %s", nickname_utf8, tag_utf8); const char *target = msg->params.vector[0]; const char *recipient = nickname; if (irc_is_channel (s, target)) recipient = target; if (!strcmp (chunk->tag.str, "CLIENTINFO")) irc_send_ctcp_reply (s, recipient, "CLIENTINFO %s %s %s %s", "PING", "VERSION", "TIME", "CLIENTINFO"); else if (!strcmp (chunk->tag.str, "PING")) irc_send_ctcp_reply (s, recipient, "PING %s", chunk->text.str); else if (!strcmp (chunk->tag.str, "VERSION")) { struct utsname info; if (uname (&info)) LOG_LIBC_FAILURE ("uname"); else irc_send_ctcp_reply (s, recipient, "VERSION %s %s on %s %s", PROGRAM_NAME, PROGRAM_VERSION, info.sysname, info.machine); } else if (!strcmp (chunk->tag.str, "TIME")) { char buf[26]; if (!ctime_now (buf)) LOG_LIBC_FAILURE ("asctime_r"); else irc_send_ctcp_reply (s, recipient, "TIME %s", buf); } free (nickname); free (nickname_utf8); free (tag_utf8); } static void irc_handle_privmsg_text (struct server *s, const struct irc_message *msg, struct str *text, bool is_action) { const char *target = msg->params.vector[0]; struct buffer *buffer = irc_get_buffer_for_message (s, msg, target); if (buffer) { // TODO: some more obvious indication of highlights int flags = irc_is_highlight (s, text->str) ? BUFFER_LINE_HIGHLIGHT : 0; enum buffer_line_type type = is_action ? BUFFER_LINE_ACTION : BUFFER_LINE_PRIVMSG; buffer_send (s->ctx, buffer, type, flags, .who = irc_to_utf8 (s->ctx, msg->prefix), .text = irc_to_utf8 (s->ctx, text->str)); } } static void irc_handle_privmsg (struct server *s, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 2) return; // This ignores empty messages which we should never receive anyway struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]); LIST_FOR_EACH (struct ctcp_chunk, iter, chunks) if (!iter->is_extended) irc_handle_privmsg_text (s, msg, &iter->text, false); else if (!strcmp (iter->tag.str, "ACTION")) irc_handle_privmsg_text (s, msg, &iter->text, true); else irc_handle_ctcp_request (s, msg, iter); ctcp_destroy (chunks); } static void irc_handle_quit (struct server *s, const struct irc_message *msg) { if (!msg->prefix) return; // What the fuck if (irc_is_this_us (s, msg->prefix)) return; char *nickname = irc_cut_nickname (msg->prefix); struct user *user = str_map_find (&s->irc_users, nickname); free (nickname); if (!user) return; const char *message = ""; if (msg->params.len > 0) message = msg->params.vector[0]; // Log a message in any PM buffer struct buffer *buffer = str_map_find (&s->irc_buffer_map, user->nickname); if (buffer) { buffer_send (s->ctx, buffer, BUFFER_LINE_QUIT, 0, .who = irc_to_utf8 (s->ctx, msg->prefix), .reason = irc_to_utf8 (s->ctx, message)); // TODO: set some kind of a flag in the buffer and when the user // reappers on a channel (JOIN), log a "is back online" message. // Also set this flag when we receive a "no such nick" numeric // and reset it when we send something to the buffer. } // Log a message in all channels the user is in LIST_FOR_EACH (struct user_channel, iter, user->channels) { buffer = str_map_find (&s->irc_buffer_map, iter->channel->name); if (buffer) buffer_send (s->ctx, buffer, BUFFER_LINE_QUIT, 0, .who = irc_to_utf8 (s->ctx, msg->prefix), .reason = irc_to_utf8 (s->ctx, message)); // This destroys "iter" which doesn't matter to us irc_remove_user_from_channel (user, iter->channel); } } static void irc_handle_topic (struct server *s, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 2) return; const char *channel_name = msg->params.vector[0]; const char *topic = msg->params.vector[1]; if (!irc_is_channel (s, channel_name)) return; struct channel *channel = str_map_find (&s->irc_channels, channel_name); struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name); hard_assert ((channel && buffer) || (channel && !buffer) || (!channel && !buffer)); // It would be is weird for this to be false if (channel) { free (channel->topic); channel->topic = xstrdup (topic); } if (buffer) { buffer_send (s->ctx, buffer, BUFFER_LINE_TOPIC, 0, .who = irc_to_utf8 (s->ctx, msg->prefix), .text = irc_to_utf8 (s->ctx, topic)); } } static struct irc_handler { char *name; void (*handler) (struct server *s, const struct irc_message *msg); } g_irc_handlers[] = { // This list needs to stay sorted { "JOIN", irc_handle_join }, { "KICK", irc_handle_kick }, { "MODE", irc_handle_mode }, { "NICK", irc_handle_nick }, { "NOTICE", irc_handle_notice }, { "PART", irc_handle_part }, { "PING", irc_handle_ping }, { "PRIVMSG", irc_handle_privmsg }, { "QUIT", irc_handle_quit }, { "TOPIC", irc_handle_topic }, }; static int irc_handler_cmp_by_name (const void *a, const void *b) { const struct irc_handler *first = a; const struct irc_handler *second = b; return strcasecmp_ascii (first->name, second->name); } static bool irc_try_parse_word_for_userhost (struct server *s, const char *word) { regex_t re; int err = regcomp (&re, "^[^!@]+!([^!@]+@[^!@]+)$", REG_EXTENDED); if (!soft_assert (!err)) return false; regmatch_t matches[2]; bool result = false; if (!regexec (&re, word, 2, matches, 0)) { free (s->irc_user_host); s->irc_user_host = xstrndup (word + matches[1].rm_so, matches[1].rm_eo - matches[1].rm_so); result = true; } regfree (&re); return result; } static void irc_try_parse_welcome_for_userhost (struct server *s, const char *m) { struct str_vector v; str_vector_init (&v); split_str_ignore_empty (m, ' ', &v); for (size_t i = 0; i < v.len; i++) if (irc_try_parse_word_for_userhost (s, v.vector[i])) break; str_vector_free (&v); } static void irc_process_numeric (struct server *s, const struct irc_message *msg, unsigned long numeric) { // Numerics typically have human-readable information // TODO: try to output certain replies in more specific buffers // Get rid of the first parameter, if there's any at all, // as it contains our nickname and is of no practical use to the user struct str_vector copy; str_vector_init (©); str_vector_add_vector (©, msg->params.vector + !!msg->params.len); // Join the parameter vector back, recode it to our internal encoding // and send it to the server buffer char *reconstructed = join_str_vector (©, ' '); str_vector_free (©); buffer_send (s->ctx, s->buffer, BUFFER_LINE_STATUS, 0, .text = irc_to_utf8 (s->ctx, reconstructed)); free (reconstructed); switch (numeric) { case IRC_RPL_WELCOME: // We still issue a USERHOST anyway as this is in general unreliable if (msg->params.len == 2) irc_try_parse_welcome_for_userhost (s, msg->params.vector[1]); break; case IRC_RPL_ISUPPORT: // TODO: parse this, mainly PREFIX; see // http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt break; case IRC_RPL_NAMREPLY: // TODO: find the channel and if found, push nicks to names_buf break; case IRC_RPL_ENDOFNAMES: // TODO: find the channel and if found, overwrite users; // however take care to combine channel user modes break; case IRC_ERR_NICKNAMEINUSE: // TODO: if not connected yet (irc_ready), use a different nick; // either use a number suffix, or accept commas in "nickname" config break; } } static void irc_process_message (const struct irc_message *msg, const char *raw, void *user_data) { struct server *s = user_data; if (g_debug_mode) { input_hide (&s->ctx->input); char *term = irc_to_term (s->ctx, raw); fprintf (stderr, "[IRC] ==> \"%s\"\n", term); free (term); input_show (&s->ctx->input); } // XXX: or is the 001 numeric enough? For what? if (!s->irc_ready && (!strcasecmp (msg->command, "MODE") || !strcasecmp (msg->command, "376") // RPL_ENDOFMOTD || !strcasecmp (msg->command, "422"))) // ERR_NOMOTD { // XXX: should we really print this? buffer_send_status (s->ctx, s->buffer, "Successfully connected"); s->irc_ready = true; refresh_prompt (s->ctx); // TODO: parse any response and store the result for us in app_context; // this enables proper message splitting on output; // we can also use WHOIS if it's not supported (optional by RFC 2812) irc_send (s, "USERHOST %s", s->irc_user->nickname); const char *autojoin = get_config_string (s->ctx, "server.autojoin"); if (autojoin) irc_send (s, "JOIN :%s", autojoin); } struct irc_handler key = { .name = msg->command }; struct irc_handler *handler = bsearch (&key, g_irc_handlers, N_ELEMENTS (g_irc_handlers), sizeof key, irc_handler_cmp_by_name); if (handler) handler->handler (s, msg); unsigned long numeric; if (xstrtoul (&numeric, msg->command, 10)) irc_process_numeric (s, msg, numeric); } // --- Message autosplitting magic --------------------------------------------- // This is the most basic acceptable algorithm; something like ICU with proper // locale specification would be needed to make it work better. static size_t wrap_text_for_single_line (const char *text, size_t text_len, size_t line_len, struct str *output) { int eaten = 0; // First try going word by word const char *word_start; const char *word_end = text + strcspn (text, " "); size_t word_len = word_end - text; while (line_len && word_len <= line_len) { if (word_len) { str_append_data (output, text, word_len); text += word_len; eaten += word_len; line_len -= word_len; } // Find the next word's end word_start = text + strspn (text, " "); word_end = word_start + strcspn (word_start, " "); word_len = word_end - text; } if (eaten) // Discard whitespace between words if split return eaten + (word_start - text); // And if that doesn't help, cut the longest valid block of characters while (true) { const char *next = utf8_next (text, text_len - eaten, NULL); hard_assert (next); size_t char_len = next - text; if (char_len > line_len) break; str_append_data (output, text, char_len); text += char_len; eaten += char_len; line_len -= char_len; } return eaten; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static bool wrap_message (const char *message, int line_max, struct str_vector *output, struct error **e) { if (line_max <= 0) goto error; for (size_t message_left = strlen (message); message_left; ) { struct str m; str_init (&m); size_t eaten = wrap_text_for_single_line (message, MIN ((size_t) line_max, message_left), message_left, &m); if (!eaten) { str_free (&m); goto error; } str_vector_add_owned (output, str_steal (&m)); message += eaten; message_left -= eaten; } return true; error: // Well, that's just weird error_set (e, "Message splitting was unsuccessful as there was " "too little room for UTF-8 characters"); return false; } /// Automatically splits messages that arrive at other clients with our prefix /// so that they don't arrive cut off by the server static bool irc_autosplit_message (struct server *s, const char *message, int fixed_part, struct str_vector *output, struct error **e) { // :!@ int space_in_one_message = 0; if (s->irc_user_host) space_in_one_message = 510 - 1 - (int) strlen (s->irc_user->nickname) - 1 - (int) strlen (s->irc_user_host) - 1 - fixed_part; // However we don't always have the full info for message splitting if (!space_in_one_message) str_vector_add (output, message); else if (!wrap_message (message, space_in_one_message, output, e)) return false; return true; } struct send_autosplit_args; typedef void (*send_autosplit_logger_fn) (struct server *s, struct send_autosplit_args *args, struct buffer *buffer, const char *line); struct send_autosplit_args { const char *command; ///< E.g. PRIVMSG or NOTICE const char *target; ///< User or channel const char *message; ///< A message to be autosplit send_autosplit_logger_fn logger; ///< Logger for all resulting lines const char *prefix; ///< E.g. "\x01ACTION" const char *suffix; ///< E.g. "\x01" }; static void send_autosplit_message (struct server *s, struct send_autosplit_args a) { struct buffer *buffer = str_map_find (&s->irc_buffer_map, a.target); int fixed_part = strlen (a.command) + 1 + strlen (a.target) + 1 + 1 + strlen (a.prefix) + strlen (a.suffix); struct str_vector lines; str_vector_init (&lines); struct error *e = NULL; if (!irc_autosplit_message (s, a.message, fixed_part, &lines, &e)) { buffer_send_error (s->ctx, buffer ? buffer : s->buffer, "%s", e->message); error_free (e); goto end; } for (size_t i = 0; i < lines.len; i++) { irc_send (s, "%s %s :%s%s%s", a.command, a.target, a.prefix, lines.vector[i], a.suffix); a.logger (s, &a, buffer, lines.vector[i]); } end: str_vector_free (&lines); } static void log_outcoming_action (struct server *s, struct send_autosplit_args *a, struct buffer *buffer, const char *line) { (void) a; if (buffer) buffer_send (s->ctx, buffer, BUFFER_LINE_ACTION, 0, .who = irc_to_utf8 (s->ctx, s->irc_user->nickname), .text = irc_to_utf8 (s->ctx, line)); // This can only be sent from a user or channel buffer } #define SEND_AUTOSPLIT_ACTION(s, target, message) \ send_autosplit_message ((s), (struct send_autosplit_args) \ { "PRIVMSG", (target), (message), log_outcoming_action, \ "\x01" "ACTION ", "\x01" }) static void log_outcoming_privmsg (struct server *s, struct send_autosplit_args *a, struct buffer *buffer, const char *line) { if (buffer) buffer_send (s->ctx, buffer, BUFFER_LINE_PRIVMSG, 0, .who = irc_to_utf8 (s->ctx, s->irc_user->nickname), .text = irc_to_utf8 (s->ctx, line)); else // TODO: fix logging and encoding buffer_send (s->ctx, s->buffer, BUFFER_LINE_STATUS, 0, .text = xstrdup_printf ("MSG(%s): %s", a->target, line)); } #define SEND_AUTOSPLIT_PRIVMSG(s, target, message) \ send_autosplit_message ((s), (struct send_autosplit_args) \ { "PRIVMSG", (target), (message), log_outcoming_privmsg, "", "" }) static void log_outcoming_notice (struct server *s, struct send_autosplit_args *a, struct buffer *buffer, const char *line) { if (buffer) buffer_send (s->ctx, buffer, BUFFER_LINE_NOTICE, 0, .who = irc_to_utf8 (s->ctx, s->irc_user->nickname), .text = irc_to_utf8 (s->ctx, line)); else // TODO: fix logging and encoding buffer_send (s->ctx, s->buffer, BUFFER_LINE_STATUS, 0, .text = xstrdup_printf ("Notice -> %s: %s", a->target, line)); } #define SEND_AUTOSPLIT_NOTICE(s, target, message) \ send_autosplit_message ((s), (struct send_autosplit_args) \ { "NOTICE", (target), (message), log_outcoming_notice, "", "" }) // --- Configuration dumper ---------------------------------------------------- struct config_dump_level { struct config_dump_level *next; ///< Next print level const char *name; ///< Name of the object }; struct config_dump_data { struct config_dump_level *head; ///< The first level struct config_dump_level **tail; ///< Where to place further levels struct str_vector *output; ///< Where to place new entries }; static void config_dump_item (struct config_item_ *item, struct config_dump_data *data); static void config_dump_children (struct config_item_ *object, struct config_dump_data *data) { hard_assert (object->type = CONFIG_ITEM_OBJECT); struct config_dump_level level; level.next = NULL; struct config_dump_level **prev_tail = data->tail; *data->tail = &level; data->tail = &level.next; struct str_map_iter iter; str_map_iter_init (&iter, &object->value.object); struct config_item_ *child; while ((child = str_map_iter_next (&iter))) { level.name = iter.link->key; config_dump_item (child, data); } data->tail = prev_tail; *data->tail = NULL; } static void config_dump_item (struct config_item_ *item, struct config_dump_data *data) { struct str line; str_init (&line); struct config_dump_level *iter = data->head; if (iter) { str_append (&line, iter->name); iter = iter->next; } for (; iter; iter = iter->next) str_append_printf (&line, ".%s", iter->name); // Empty objects will show as such if (item->type == CONFIG_ITEM_OBJECT && item->value.object.len) { config_dump_children (item, data); return; } // Don't bother writing out null values everywhere struct config_schema *schema = item->schema; bool has_default = schema && schema->default_; if (item->type != CONFIG_ITEM_NULL || has_default) { str_append (&line, " = "); struct str value; str_init (&value); config_item_write (item, false, &value); str_append_str (&line, &value); if (has_default && strcmp (schema->default_, value.str)) str_append_printf (&line, " (default: %s)", schema->default_); else if (!has_default && item->type != CONFIG_ITEM_NULL) str_append_printf (&line, " (default: %s)", "null"); str_free (&value); } str_vector_add_owned (data->output, str_steal (&line)); } static void config_dump (struct config_item_ *root, struct str_vector *output) { struct config_dump_data data; data.head = NULL; data.tail = &data.head; data.output = output; config_dump_item (root, &data); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static int str_vector_sort_cb (const void *a, const void *b) { return strcmp (*(const char **) a, *(const char **) b); } static void str_vector_sort (struct str_vector *self) { qsort (self->vector, self->len, sizeof *self->vector, str_vector_sort_cb); } static void dump_matching_options (struct app_context *ctx, const char *mask, struct str_vector *output) { config_dump (ctx->config.root, output); str_vector_sort (output); // Filter out results by wildcard matching for (size_t i = 0; i < output->len; i++) { // Yeah, I know const char *line = output->vector[i]; char *key = xstrndup (line, strcspn (line, " ")); if (fnmatch (mask, key, 0)) str_vector_remove (output, i--); free (key); } } // --- User input handling ----------------------------------------------------- static bool handle_command_help (struct app_context *, char *); /// Cuts the longest non-whitespace portion of text and advances the pointer static char * cut_word (char **s) { char *start = *s; size_t word_len = strcspn (*s, WORD_BREAKING_CHARS); char *end = start + word_len; *s = end + strspn (end, WORD_BREAKING_CHARS); *end = '\0'; return start; } static bool try_handle_buffer_goto (struct app_context *ctx, const char *word) { unsigned long n; if (!xstrtoul (&n, word, 10)) return false; if (n > INT_MAX || !buffer_goto (ctx, n)) buffer_send_error (ctx, ctx->global_buffer, "%s: %s", "no such buffer", word); return true; } static struct buffer * try_decode_buffer (struct app_context *ctx, const char *word) { unsigned long n; struct buffer *buffer = NULL; if (xstrtoul (&n, word, 10) && n <= INT_MAX) buffer = buffer_at_index (ctx, n); if (!buffer) buffer = buffer_by_name (ctx, word); // TODO: decode the global and server buffers, partial matches return buffer; } static bool server_command_check (struct app_context *ctx, const char *action) { if (ctx->current_buffer->type == BUFFER_GLOBAL) buffer_send_error (ctx, ctx->current_buffer, "Can't do this from a global buffer (%s)", action); else { struct server *s = ctx->current_buffer->server; if (s->irc_fd == -1) buffer_send_error (ctx, s->buffer, "Not connected"); else return true; } return false; } static void show_buffers_list (struct app_context *ctx) { buffer_send_status (ctx, ctx->global_buffer, "%s", ""); buffer_send_status (ctx, ctx->global_buffer, "Buffers list:"); int i = 1; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) buffer_send_status (ctx, ctx->global_buffer, " [%d] %s", i++, iter->name); } static void handle_buffer_close (struct app_context *ctx, char *arguments) { struct buffer *buffer = NULL; const char *which = NULL; if (!*arguments) buffer = ctx->current_buffer; else buffer = try_decode_buffer (ctx, (which = cut_word (&arguments))); if (!buffer) buffer_send_error (ctx, ctx->global_buffer, "%s: %s", "No such buffer", which); else if (buffer == ctx->global_buffer) buffer_send_error (ctx, ctx->global_buffer, "Can't close the global buffer"); else if (buffer->type == BUFFER_SERVER) buffer_send_error (ctx, ctx->global_buffer, "Can't close a server buffer"); else { if (buffer == ctx->current_buffer) buffer_activate (ctx, buffer_next (ctx, 1)); buffer_remove (ctx, buffer); } } static bool handle_command_buffer (struct app_context *ctx, char *arguments) { char *action = cut_word (&arguments); if (try_handle_buffer_goto (ctx, action)) return true; // XXX: also build a prefix map? // TODO: some subcommand to print N last lines from the buffer if (!strcasecmp_ascii (action, "list")) show_buffers_list (ctx); else if (!strcasecmp_ascii (action, "clear")) { // TODO } else if (!strcasecmp_ascii (action, "move")) { // TODO: unlink the buffer and link it back at index; // we will probably need to extend liberty for this } else if (!strcasecmp_ascii (action, "close")) handle_buffer_close (ctx, arguments); else return false; return true; } static bool replace_string_array (struct config_item_ *item, struct str_vector *array, struct error **e) { char *changed = join_str_vector (array, ','); struct str tmp = { .str = changed, .len = strlen (changed) }; bool result = config_item_set_from (item, config_item_string_array (&tmp), e); free (changed); return result; } static bool handle_command_set_add (struct config_item_ *item, const char *value, struct error **e) { bool result = false; struct str_vector items; str_vector_init (&items); split_str (item->value.string.str, ',', &items); if (items.len == 1 && !*items.vector[0]) str_vector_reset (&items); if (str_vector_find (&items, value) != -1) error_set (e, "already present in the array: %s", value); else { str_vector_add (&items, value); result = replace_string_array (item, &items, e); } str_vector_free (&items); return result; } static bool handle_command_set_remove (struct config_item_ *item, const char *value, struct error **e) { bool result = false; struct str_vector items; str_vector_init (&items); split_str (item->value.string.str, ',', &items); if (items.len == 1 && !*items.vector[0]) str_vector_reset (&items); ssize_t i = str_vector_find (&items, value); if (i == -1) error_set (e, "not present in the array: %s", value); else { str_vector_remove (&items, i); result = replace_string_array (item, &items, e); } str_vector_free (&items); return result; } static void handle_command_set_assign_item (struct app_context *ctx, char *key, struct config_item_ *new_, bool add, bool remove) { struct config_item_ *item = config_item_get (ctx->config.root, key, NULL); hard_assert (item); struct error *e = NULL; if ((add | remove) && item->type != CONFIG_ITEM_STRING_ARRAY) // FIXME: it can also be null, which makes this message confusing error_set (&e, "not a string array"); else if (add) handle_command_set_add (item, new_->value.string.str, &e); else if (remove) handle_command_set_remove (item, new_->value.string.str, &e); else config_item_set_from (item, config_item_clone (new_), &e); if (e) { buffer_send_error (ctx, ctx->global_buffer, "Failed to set option \"%s\": %s", key, e->message); error_free (e); } else { struct str_vector tmp; str_vector_init (&tmp); dump_matching_options (ctx, key, &tmp); buffer_send_status (ctx, ctx->global_buffer, "Option changed: %s", tmp.vector[0]); str_vector_free (&tmp); } } static bool handle_command_set_assign (struct app_context *ctx, struct str_vector *all, char *arguments) { char *op = cut_word (&arguments); bool add = false; bool remove = false; if (!strcmp (op, "+=")) add = true; else if (!strcmp (op, "-=")) remove = true; else if (strcmp (op, "=")) return false; if (!arguments) return false; struct error *e = NULL; struct config_item_ *new_ = config_item_parse (arguments, strlen (arguments), true, &e); if (e) { buffer_send_error (ctx, ctx->global_buffer, "Invalid value: %s", e->message); error_free (e); return true; } if ((add | remove) && !config_item_type_is_string (new_->type)) { buffer_send_error (ctx, ctx->global_buffer, "+= / -= operators need a string argument"); config_item_destroy (new_); return true; } for (size_t i = 0; i < all->len; i++) { char *key = xstrndup (all->vector[i], strcspn (all->vector[i], " ")); handle_command_set_assign_item (ctx, key, new_, add, remove); free (key); } config_item_destroy (new_); return true; } static bool handle_command_set (struct app_context *ctx, char *arguments) { char *option = "*"; if (*arguments) option = cut_word (&arguments); struct str_vector all; str_vector_init (&all); dump_matching_options (ctx, option, &all); bool result = true; if (!all.len) buffer_send_error (ctx, ctx->global_buffer, "No matches: %s", option); else if (!*arguments) { buffer_send_status (ctx, ctx->global_buffer, "%s", ""); for (size_t i = 0; i < all.len; i++) buffer_send_status (ctx, ctx->global_buffer, "%s", all.vector[i]); } else result = handle_command_set_assign (ctx, &all, arguments); str_vector_free (&all); return result; } static bool handle_command_save (struct app_context *ctx, char *arguments) { if (*arguments) return false; struct str data; str_init (&data); serialize_configuration (ctx, &data); struct error *e = NULL; char *filename = write_configuration_file (&data, &e); str_free (&data); if (!filename) { buffer_send_error (ctx, ctx->global_buffer, "%s: %s", "Saving configuration failed", e->message); error_free (e); } else buffer_send_status (ctx, ctx->global_buffer, "Configuration written to `%s'", filename); free (filename); return true; } static bool handle_command_msg (struct app_context *ctx, char *arguments) { if (!server_command_check (ctx, "send messages")) return true; if (!*arguments) return false; struct server *s = ctx->current_buffer->server; char *target = cut_word (&arguments); if (!*arguments) buffer_send_error (ctx, s->buffer, "No text to send"); else SEND_AUTOSPLIT_PRIVMSG (s, target, arguments); return true; } static bool handle_command_query (struct app_context *ctx, char *arguments) { if (!server_command_check (ctx, "send messages")) return true; if (!*arguments) return false; struct server *s = ctx->current_buffer->server; char *target = cut_word (&arguments); if (irc_is_channel (s, target)) buffer_send_error (ctx, s->buffer, "Cannot query a channel"); else if (!*arguments) buffer_send_error (ctx, s->buffer, "No text to send"); else { buffer_activate (ctx, irc_get_or_make_user_buffer (s, target)); SEND_AUTOSPLIT_PRIVMSG (s, target, arguments); } return true; } static bool handle_command_notice (struct app_context *ctx, char *arguments) { if (!server_command_check (ctx, "send messages")) return true; if (!*arguments) return false; struct server *s = ctx->current_buffer->server; char *target = cut_word (&arguments); if (!*arguments) buffer_send_error (ctx, s->buffer, "No text to send"); else SEND_AUTOSPLIT_NOTICE (s, target, arguments); return true; } static bool handle_command_ctcp (struct app_context *ctx, char *arguments) { if (!server_command_check (ctx, "send messages")) return true; if (!*arguments) return false; char *target = cut_word (&arguments); if (!*arguments) return false; char *tag = cut_word (&arguments); for (char *p = tag; *p; p++) *p = toupper_ascii (*p); struct server *s = ctx->current_buffer->server; if (*arguments) irc_send (s, "PRIVMSG %s :\x01%s %s\x01", target, tag, arguments); else irc_send (s, "PRIVMSG %s :\x01%s\x01", target, tag); buffer_send_status (ctx, s->buffer, "CTCP query to %s: %s", target, tag); return true; } static bool handle_command_me (struct app_context *ctx, char *arguments) { if (!server_command_check (ctx, "send messages")) return true; struct server *s = ctx->current_buffer->server; if (ctx->current_buffer->type == BUFFER_CHANNEL) SEND_AUTOSPLIT_ACTION (s, ctx->current_buffer->channel->name, arguments); else if (ctx->current_buffer->type == BUFFER_PM) SEND_AUTOSPLIT_ACTION (s, ctx->current_buffer->user->nickname, arguments); else buffer_send_error (ctx, s->buffer, "Can't do this from a server buffer (%s)", "send CTCP actions"); return true; } static bool handle_command_quit (struct app_context *ctx, char *arguments) { // TODO: multiserver struct server *s = &ctx->server; if (s->irc_fd != -1) { if (*arguments) irc_send (s, "QUIT :%s", arguments); else irc_send (s, "QUIT :%s", PROGRAM_NAME " " PROGRAM_VERSION); } initiate_quit (ctx); return true; } static bool handle_command_join (struct app_context *ctx, char *arguments) { if (!server_command_check (ctx, "join")) return true; struct server *s = ctx->current_buffer->server; if (*arguments) // TODO: check if the arguments are in the form of // "channel(,channel)* key(,key)*" irc_send (s, "JOIN %s", arguments); else { if (ctx->current_buffer->type != BUFFER_CHANNEL) buffer_send_error (ctx, ctx->current_buffer, "%s: %s", "Can't join", "no argument given and this buffer is not a channel"); // TODO: have a better way of checking if we're on the channel else if (ctx->current_buffer->channel->users) buffer_send_error (ctx, ctx->current_buffer, "%s: %s", "Can't join", "you already are on the channel"); else // TODO: send the key if known irc_send (s, "JOIN %s", ctx->current_buffer->channel->name); } return true; } static bool handle_command_part (struct app_context *ctx, char *arguments) { if (!server_command_check (ctx, "part")) return true; struct server *s = ctx->current_buffer->server; if (*arguments) // TODO: check if the arguments are in the form of "channel(,channel)*" // TODO: make sure to send the reason as one argument irc_send (s, "PART %s", arguments); else { if (ctx->current_buffer->type != BUFFER_CHANNEL) buffer_send_error (ctx, ctx->current_buffer, "%s: %s", "Can't part", "no argument given and this buffer is not a channel"); // TODO: have a better way of checking if we're on the channel else if (!ctx->current_buffer->channel->users) buffer_send_error (ctx, ctx->current_buffer, "%s: %s", "Can't join", "you're not on the channel"); else irc_send (s, "PART %s", ctx->current_buffer->channel->name); } return true; } static bool handle_command_connect (struct app_context *ctx, char *arguments) { // TODO: multiserver struct server *s = &ctx->server; if (s->irc_fd != -1) { buffer_send_error (ctx, s->buffer, "Already connected"); return true; } irc_cancel_timers (s); on_irc_reconnect_timeout (s); return true; } static bool handle_command_list (struct app_context *ctx, char *arguments) { if (!server_command_check (ctx, "list channels")) return true; struct server *s = ctx->current_buffer->server; if (*arguments) irc_send (s, "LIST %s", arguments); else irc_send (s, "LIST"); return true; } static bool handle_command_nick (struct app_context *ctx, char *arguments) { if (!server_command_check (ctx, "change nickname")) return true; if (!*arguments) return false; struct server *s = ctx->current_buffer->server; irc_send (s, "NICK %s", cut_word (&arguments)); return true; } static bool handle_command_quote (struct app_context *ctx, char *arguments) { if (!server_command_check (ctx, "quote")) return true; struct server *s = ctx->current_buffer->server; irc_send (s, "%s", arguments); return true; } static struct command_handler { const char *name; bool (*handler) (struct app_context *ctx, char *arguments); const char *description; const char *usage; } g_command_handlers[] = { { "help", handle_command_help, "Show help", "[ |