/* * 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 // FIXME: awful naming, collides with ATTRIBUTE_* #define ATTR_TABLE(XX) \ XX( PROMPT, "prompt", "Terminal attrs for the prompt" ) \ XX( RESET, "reset", "String to reset terminal attributes" ) \ XX( READ_MARKER, "read_marker", "Terminal attrs for the read marker" ) \ XX( WARNING, "warning", "Terminal attrs for warnings" ) \ XX( ERROR, "error", "Terminal attrs for errors" ) \ XX( EXTERNAL, "external", "Terminal attrs for external lines" ) \ XX( TIMESTAMP, "timestamp", "Terminal attrs for timestamps" ) \ XX( HIGHLIGHT, "highlight", "Terminal attrs for highlights" ) \ XX( ACTION, "action", "Terminal attrs for user actions" ) \ XX( USERHOST, "userhost", "Terminal attrs for user@host" ) \ XX( JOIN, "join", "Terminal attrs for joins" ) \ XX( PART, "part", "Terminal attrs 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" contents from here free (self->history); #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; if (!self->active) return; // First reset the prompt to work around a bug in readline rl_set_prompt (""); if (self->prompt_shown > 0) rl_redisplay (); rl_set_prompt (self->prompt); if (self->prompt_shown > 0) 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 void input_bind (struct input *self, const char *seq, const char *function_name) { (void) self; rl_bind_keyseq (seq, rl_named_function (function_name)); } static void input_bind_meta (struct input *self, char key, const char *function_name) { // This one seems to actually work char keyseq[] = { '\\', 'e', key, 0 }; input_bind (self, keyseq, function_name); #if 0 // While this one only fucks up UTF-8 // Tested with urxvt and xterm, on Debian Jessie/Arch, default settings // \M- behaves exactly the same rl_bind_key (META (key), rl_named_function (function_name)); #endif } static void input_bind_control (struct input *self, char key, const char *function_name) { char keyseq[] = { '\\', 'C', '-', key, 0 }; input_bind (self, keyseq, function_name); } static void input_insert_c (struct input *self, int c) { char s[2] = { c, 0 }; rl_insert_text (s); if (self->prompt_shown > 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; self->prompt_shown = 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; state->entries = NULL; 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 > 0) 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); rl_clear_history (); // rl_clear_history just removes history entries, // we have to reclaim memory for their actual container ourselves free (buffer->history->entries); free (buffer->history); buffer->history = NULL; 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_bind (struct input *self, const char *seq, const char *function_name) { el_set (self->editline, EL_BIND, seq, function_name, NULL); } static void input_bind_meta (struct input *self, char key, const char *function_name) { char keyseq[] = { 'M', '-', key, 0 }; input_bind (self, keyseq, function_name); } static void input_bind_control (struct input *self, char key, const char *function_name) { char keyseq[] = { '^', key, 0 }; input_bind (self, keyseq, function_name); } 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 > 0) 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_insert_c (struct input *self, int c) { char s[2] = { c, 0 }; el_insertstr (self->editline, s); if (self->prompt_shown > 0) 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"); 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; self->prompt_shown = 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 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 struct str prefixes; ///< Ordered @+... characters }; static struct channel_user * channel_user_new (void) { struct channel_user *self = xcalloc (1, sizeof *self); str_init (&self->prefixes); return self; } static void channel_user_destroy (struct channel_user *self) { user_unref (self->user); str_free (&self->prefixes); 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 char *name; ///< Channel name char *topic; ///< Channel topic // XXX: write something like an ordered set of characters object? struct str no_param_modes; ///< No parameter channel modes struct str_map param_modes; ///< Parametrized channel modes 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_init (&self->no_param_modes); str_map_init (&self->param_modes); self->param_modes.free = free; str_vector_init (&self->names_buf); return self; } static void channel_destroy (struct channel *self) { free (self->name); free (self->topic); str_free (&self->no_param_modes); str_map_free (&self->param_modes); // 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 formatter_item_type { FORMATTER_ITEM_TEXT, ///< Text FORMATTER_ITEM_ATTR, ///< Formatting attributes FORMATTER_ITEM_FG_COLOR, ///< Foreground color FORMATTER_ITEM_BG_COLOR, ///< Background color FORMATTER_ITEM_SIMPLE, ///< For mIRC formatting only so far FORMATTER_ITEM_IGNORE_ATTR ///< Un/set attribute ignoration }; 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 struct server *s; ///< Server struct formatter_item *items; ///< Items struct formatter_item *items_tail; ///< Tail of items }; static void formatter_init (struct formatter *self, struct app_context *ctx, struct server *s) { memset (self, 0, sizeof *self); self->ctx = ctx; self->s = s; } static void formatter_free (struct formatter *self) { LIST_FOR_EACH (struct formatter_item, iter, self->items) formatter_item_destroy (iter); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum buffer_line_flags { BUFFER_LINE_STATUS = 1 << 0, ///< Status message BUFFER_LINE_ERROR = 1 << 1, ///< Error message BUFFER_LINE_HIGHLIGHT = 1 << 2, ///< The user was highlighted by this BUFFER_LINE_SKIP_FILE = 1 << 3, ///< Don't log this to file BUFFER_LINE_INDENT = 1 << 4 ///< Just indent the line }; struct buffer_line { LIST_HEADER (struct buffer_line) int flags; ///< Flags time_t when; ///< Time of the event struct formatter *formatter; ///< Line data }; 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) { if (self->formatter) formatter_free (self->formatter); free (self->formatter); 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 bool highlighted; ///< We've been highlighted FILE *log_file; ///< Log file // 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->log_file) (void) fclose (self->log_file); if (self->user) user_unref (self->user); if (self->channel) channel_unref (self->channel); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum transport_io_result { TRANSPORT_IO_OK = 0, ///< Completed successfully TRANSPORT_IO_EOF, ///< Connection shut down by peer TRANSPORT_IO_ERROR ///< Connection error }; // The only real purpose of this is to abstract away TLS/SSL struct transport { /// Initialize the transport bool (*init) (struct server *s, struct error **e); /// Destroy the user data pointer void (*cleanup) (struct server *s); /// The underlying socket may have become readable, update `read_buffer' enum transport_io_result (*try_read) (struct server *s); /// The underlying socket may have become writeable, flush `write_buffer' enum transport_io_result (*try_write) (struct server *s); /// Return event mask to use in the poller int (*get_poll_events) (struct server *s); /// Called just before closing the connection from our side void (*in_before_shutdown) (struct server *s); }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum server_state { IRC_DISCONNECTED, ///< Not connected IRC_CONNECTING, ///< Connecting to the server IRC_CONNECTED, ///< Trying to register IRC_REGISTERED, ///< We can chat now IRC_CLOSING, ///< Flushing output before shutdown IRC_HALF_CLOSED ///< Connection shutdown from our side }; /// Convert an IRC identifier character to lower-case typedef int (*irc_tolower_fn) (int); /// Key conversion function for hashmap lookups typedef size_t (*irc_strxfrm_fn) (char *, const char *, size_t); struct server { struct app_context *ctx; ///< Application context char *name; ///< Server identifier struct buffer *buffer; ///< The buffer for this server struct config_item_ *config; ///< Configuration root // Connection: enum server_state state; ///< Connection state struct connector *connector; ///< Connection establisher bool manual_disconnect; ///< Don't reconnect after disconnect int socket; ///< Socket FD of the server struct str read_buffer; ///< Input yet to be processed struct str write_buffer; ///< Outut yet to be be sent out struct poller_fd socket_event; ///< We can read from the socket struct transport *transport; ///< Transport method void *transport_data; ///< Transport data // 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 // IRC: // TODO: an output queue to prevent excess floods (this will be needed // especially for away status polling) bool rehashing; ///< Rehashing IRC identifiers 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 int nick_counter; ///< Iterates "nicks" when registering struct str irc_user_mode; ///< Our current user modes char *irc_user_host; ///< Our current user@host bool cap_echo_message; ///< Whether the server echos messages // Server-specific information (from RPL_ISUPPORT): irc_tolower_fn irc_tolower; ///< Server tolower() irc_strxfrm_fn irc_strxfrm; ///< Server strxfrm() char *irc_chantypes; ///< Channel types (name prefixes) char *irc_idchan_prefixes; ///< Prefixes for "safe channels" char *irc_statusmsg; ///< Prefixes for channel targets char *irc_chanmodes_list; ///< Channel modes for lists char *irc_chanmodes_param_always; ///< Channel modes with mandatory param char *irc_chanmodes_param_when_set; ///< Channel modes with param when set char *irc_chanmodes_param_never; ///< Channel modes without param char *irc_chanuser_prefixes; ///< Channel user prefixes char *irc_chanuser_modes; ///< Channel user modes unsigned irc_max_modes; ///< Max parametrized modes per command }; static void on_irc_timeout (void *user_data); static void on_irc_ping_timeout (void *user_data); static void irc_initiate_connect (struct server *s); static void server_init_specifics (struct server *self) { // Defaults as per the RPL_ISUPPORT drafts, or RFC 1459 self->irc_tolower = irc_tolower; self->irc_strxfrm = irc_strxfrm; self->irc_chantypes = xstrdup ("#&"); self->irc_idchan_prefixes = xstrdup (""); self->irc_statusmsg = xstrdup (""); self->irc_chanmodes_list = xstrdup ("b"); self->irc_chanmodes_param_always = xstrdup ("k"); self->irc_chanmodes_param_when_set = xstrdup ("l"); self->irc_chanmodes_param_never = xstrdup ("imnpst"); self->irc_chanuser_prefixes = xstrdup ("@+"); self->irc_chanuser_modes = xstrdup ("ov"); self->irc_max_modes = 3; } static void server_free_specifics (struct server *self) { free (self->irc_chantypes); free (self->irc_idchan_prefixes); free (self->irc_statusmsg); free (self->irc_chanmodes_list); free (self->irc_chanmodes_param_always); free (self->irc_chanmodes_param_when_set); free (self->irc_chanmodes_param_never); free (self->irc_chanuser_prefixes); free (self->irc_chanuser_modes); } static void server_init (struct server *self, struct poller *poller) { memset (self, 0, sizeof *self); self->socket = -1; str_init (&self->read_buffer); str_init (&self->write_buffer); self->state = IRC_DISCONNECTED; 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 = (poller_timer_fn) irc_initiate_connect; self->reconnect_tmr.user_data = self; 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; str_init (&self->irc_user_mode); server_free_specifics (self); server_init_specifics (self); } static void server_free (struct server *self) { free (self->name); if (self->connector) { connector_free (self->connector); free (self->connector); } if (self->transport && self->transport->cleanup) self->transport->cleanup (self); if (self->socket != -1) { xclose (self->socket); self->socket_event.closed = true; poller_fd_reset (&self->socket_event); } str_free (&self->read_buffer); str_free (&self->write_buffer); str_map_free (&self->irc_users); str_map_free (&self->irc_channels); str_map_free (&self->irc_buffer_map); if (self->irc_user) user_unref (self->irc_user); str_free (&self->irc_user_mode); free (self->irc_user_host); server_free_specifics (self); } static void server_destroy (void *self) { server_free (self); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct app_context { bool no_colors; ///< Disable attribute printing char *attrs_defaults[ATTR_COUNT]; ///< Default terminal attributes // Configuration: struct config config; ///< Program configuration char *attrs[ATTR_COUNT]; ///< Terminal attributes bool isolate_buffers; ///< Isolate global/server buffers bool beep_on_highlight; ///< Beep on highlight bool logging; ///< Logging to file enabled struct str_map servers; ///< Our servers // Events: struct poller_fd tty_event; ///< Terminal input event struct poller_fd signal_event; ///< Signal FD event struct poller_timer flush_timer; ///< Flush all open files (e.g. logs) struct poller_timer date_chg_tmr; ///< Print a date change 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 *global_buffer; ///< The global buffer struct buffer *current_buffer; ///< The current buffer struct buffer *last_buffer; ///< Last used buffer // TODO: make buffer names fully unique like weechat does struct str_map buffers_by_name; ///< Buffers by name 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 bool awaiting_mirc_escape; ///< Awaiting a mIRC attribute escape char char_buf[MB_LEN_MAX + 1]; ///< Buffered multibyte char size_t char_buf_len; ///< How much of an MB char is buffered } *g_ctx; static void app_context_init (struct app_context *self) { memset (self, 0, sizeof *self); config_init (&self->config); poller_init (&self->poller); str_map_init (&self->servers); self->servers.free = server_destroy; self->servers.key_xfrm = tolower_ascii_strxfrm; str_map_init (&self->buffers_by_name); self->buffers_by_name.key_xfrm = tolower_ascii_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_defaults[i]); free (self->attrs[i]); } LIST_FOR_EACH (struct buffer, iter, self->buffers) { #ifdef HAVE_READLINE input_destroy_buffer (&self->input, iter->input_data); iter->input_data = NULL; #endif // HAVE_READLINE buffer_destroy (iter); } str_map_free (&self->buffers_by_name); str_map_free (&self->servers); 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); // --- Configuration ----------------------------------------------------------- static void on_config_debug_mode_change (struct config_item_ *item) { g_debug_mode = item->value.boolean; } static void on_config_attribute_change (struct config_item_ *item); static void on_config_logging_change (struct config_item_ *item); #define TRIVIAL_BOOLEAN_ON_CHANGE(name) \ static void \ on_config_ ## name ## _change (struct config_item_ *item) \ { \ struct app_context *ctx = item->user_data; \ ctx->name = item->value.boolean; \ } TRIVIAL_BOOLEAN_ON_CHANGE (isolate_buffers) TRIVIAL_BOOLEAN_ON_CHANGE (beep_on_highlight) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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_addresses (const struct config_item_ *item, struct error **e) { if (item->type == CONFIG_ITEM_NULL) return true; if (!config_validate_nonjunk_string (item, e)) return false; // Comma-separated list of "host[:port]" pairs regex_t re; int err = regcomp (&re, "^([^/:,]+(:[^/:,]+)?)?" "(,([^/:,]+(:[^/:,]+)?)?)*$", REG_EXTENDED | REG_NOSUB); hard_assert (!err); bool result = !regexec (&re, item->value.string.str, 0, NULL, 0); if (!result) error_set (e, "invalid address list string"); regfree (&re); return result; } 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; } static struct config_schema g_config_server[] = { { .name = "nicks", .comment = "IRC nickname", .type = CONFIG_ITEM_STRING_ARRAY, .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 = "addresses", .comment = "Addresses of the IRC network (e.g. \"irc.net:6667\")", .type = CONFIG_ITEM_STRING_ARRAY, .validate = config_validate_addresses }, { .name = "password", .comment = "Password to connect to the server, if any", .type = CONFIG_ITEM_STRING, .validate = config_validate_nonjunk_string }, { .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 = "autoconnect", .comment = "Connect automatically on startup", .type = CONFIG_ITEM_BOOLEAN, .default_ = "on" }, { .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, .validate = config_validate_nonnegative, .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 }, {} }; static 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", .on_change = on_config_isolate_buffers_change }, { .name = "beep_on_highlight", .comment = "Beep when highlighted or on a new invisible PM", .type = CONFIG_ITEM_BOOLEAN, .default_ = "on", .on_change = on_config_beep_on_highlight_change }, { .name = "logging", .comment = "Log buffer contents to file", .type = CONFIG_ITEM_BOOLEAN, .default_ = "off", .on_change = on_config_logging_change }, { .name = "save_on_quit", .comment = "Save configuration before quitting", .type = CONFIG_ITEM_BOOLEAN, .default_ = "on" }, { .name = "debug_mode", .comment = "Produce some debugging output", .type = CONFIG_ITEM_BOOLEAN, .default_ = "off", .on_change = on_config_debug_mode_change }, {} }; static struct config_schema g_config_attributes[] = { #define XX(x, y, z) { .name = y, .comment = z, .type = CONFIG_ITEM_STRING, \ .on_change = on_config_attribute_change }, ATTR_TABLE (XX) #undef XX {} }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void load_config_behaviour (struct config_item_ *subtree, void *user_data) { config_schema_apply_to_object (g_config_behaviour, subtree, user_data); } static void load_config_attributes (struct config_item_ *subtree, void *user_data) { config_schema_apply_to_object (g_config_attributes, subtree, user_data); } static void register_config_modules (struct app_context *ctx) { struct config *config = &ctx->config; // The servers are loaded later when we can create buffers for them config_register_module (config, "servers", NULL, NULL); config_register_module (config, "aliases", NULL, NULL); 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 config_item_ *root, const char *key) { struct config_item_ *item = config_item_get (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 config_item_ *root, const char *key, const char *value) { struct config_item_ *item = config_item_get (root, key, NULL); hard_assert (item); struct config_item_ *new_ = config_item_string_from_cstr (value); 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 config_item_ *root, const char *key) { struct config_item_ *item = config_item_get (root, key, NULL); hard_assert (item && item->type == CONFIG_ITEM_INTEGER); return item->value.integer; } static bool get_config_boolean (struct config_item_ *root, const char *key) { struct config_item_ *item = config_item_get (root, key, NULL); hard_assert (item && item->type == CONFIG_ITEM_BOOLEAN); return item->value.boolean; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static struct str_map * get_servers_config (struct app_context *ctx) { return &config_item_get (ctx->config.root, "servers", NULL)->value.object; } static struct str_map * get_aliases_config (struct app_context *ctx) { return &config_item_get (ctx->config.root, "aliases", NULL)->value.object; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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); } // --- Terminal output --------------------------------------------------------- /// Default color pair #define COLOR_DEFAULT -1 /// Bright versions of the basic color set #define COLOR_BRIGHT(x) (COLOR_ ## x + 8) /// Builds a color pair for 256-color terminals with a 16-color backup value #define COLOR_256(name, c256) \ (((COLOR_ ## name) & 0xFFFF) | ((c256 & 0xFFFF) << 16)) 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[256]; ///< Codes to set the foreground colour char *color_set_bg[256]; ///< 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 (); int max = MIN (256, max_colors); for (int i = 0; i < max; 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 (int i = 0; i < 256; 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 apply_attribute_change (struct config_item_ *item, int id) { struct app_context *ctx = item->user_data; free (ctx->attrs[id]); ctx->attrs[id] = xstrdup (item->type == CONFIG_ITEM_NULL ? ctx->attrs_defaults[id] : item->value.string.str); } static void on_config_attribute_change (struct config_item_ *item) { static const char *table[ATTR_COUNT] = { #define XX(x, y, z) [ATTR_ ## x] = y, ATTR_TABLE (XX) #undef XX }; for (size_t i = 0; i < N_ELEMENTS (table); i++) if (!strcmp (item->schema->name, table[i])) { apply_attribute_change (item, i); return; } } static void init_colors (struct app_context *ctx) { bool have_ti = init_terminal (); char **defaults = ctx->attrs_defaults; #define INIT_ATTR(id, ti) defaults[ATTR_ ## id] = xstrdup (have_ti ? (ti) : "") INIT_ATTR (PROMPT, enter_bold_mode); INIT_ATTR (RESET, exit_attribute_mode); INIT_ATTR (READ_MARKER, g_terminal.color_set_fg[COLOR_MAGENTA]); 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; // Apply the default values so that we start with any formatting at all config_schema_call_changed (config_item_get (ctx->config.root, "attributes", NULL)); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // A little tool that tries to make the most of the terminal's capabilities // to set up text attributes. It mostly targets just terminal emulators as that // is what people are using these days. At least no stupid ncurses limits us // with color pairs. enum { ATTRIBUTE_BOLD = 1 << 0, ATTRIBUTE_ITALIC = 1 << 1, ATTRIBUTE_UNDERLINE = 1 << 2, ATTRIBUTE_INVERSE = 1 << 3, ATTRIBUTE_BLINK = 1 << 4 }; struct attribute_printer { struct app_context *ctx; ///< Application context terminal_printer_fn printer; ///< Terminal printer bool dirty; ///< Attributes are set int want; ///< Desired attributes int want_foreground; ///< Desired foreground color int want_background; ///< Desired background color }; static void attribute_printer_reset (struct attribute_printer *self) { if (self->dirty) tputs (self->ctx->attrs[ATTR_RESET], 1, self->printer); self->dirty = false; } static void attribute_printer_init (struct attribute_printer *self, struct app_context *ctx, terminal_printer_fn printer) { self->ctx = ctx; self->printer = printer; self->dirty = true; self->want = 0; self->want_foreground = -1; self->want_background = -1; } static void attribute_printer_apply (struct attribute_printer *self, int attribute) { attribute_printer_reset (self); if (attribute != ATTR_RESET) { tputs (self->ctx->attrs[attribute], 1, self->printer); self->dirty = true; } } // NOTE: commonly terminals have: // 8 colors (worst, bright fg with BOLD, bg sometimes with BLINK) // 16 colors (okayish, we have the full basic range guaranteed) // 88 colors (the same plus a 4^3 RGB cube and a few shades of gray) // 256 colors (best, like above but with a larger cube and more gray) /// Interpolate from the 256-color palette to the 88-color one static int attribute_printer_256_to_88 (int color) { // These colours are the same everywhere if (color < 16) return color; // 24 -> 8 extra shades of gray if (color >= 232) return 80 + (color - 232) / 3; // 6 * 6 * 6 cube -> 4 * 4 * 4 cube int x[6] = { 0, 1, 1, 2, 2, 3 }; int index = color - 16; return 16 + ( x[ index / 36 ] << 8 | x[(index / 6) % 6 ] << 4 | x[(index % 6) ] ); } static int attribute_printer_decode_color (int color, bool *is_bright) { int16_t c16 = color; hard_assert (c16 < 16); int16_t c256 = color >> 16; hard_assert (c256 < 256); *is_bright = false; switch (max_colors) { case 8: if (c16 >= 8) { c16 -= 8; *is_bright = true; } case 16: return c16; case 88: return c256 <= 0 ? c16 : attribute_printer_256_to_88 (c256); case 256: return c256 <= 0 ? c16 : c256; default: // Unsupported palette return -1; } } static void attribute_printer_update (struct attribute_printer *self) { bool fg_is_bright; int fg = attribute_printer_decode_color (self->want_foreground, &fg_is_bright); bool bg_is_bright; int bg = attribute_printer_decode_color (self->want_background, &bg_is_bright); // TODO: (INVERSE | BOLD) should be used for bright backgrounds // when possible, i.e. when the foreground shouldn't be bright as well // and when the BOLD attribute hasn't already been set int attributes = self->want; if (attributes & ATTRIBUTE_INVERSE) { bool tmp = fg_is_bright; fg_is_bright = bg_is_bright; bg_is_bright = tmp; } if (fg_is_bright) attributes |= ATTRIBUTE_BOLD; if (bg_is_bright) attributes |= ATTRIBUTE_BLINK; attribute_printer_reset (self); if (attributes) tputs (tparm (set_attributes, 0, // standout attributes & ATTRIBUTE_UNDERLINE, attributes & ATTRIBUTE_INVERSE, attributes & ATTRIBUTE_BLINK, 0, // dim attributes & ATTRIBUTE_BOLD, 0, // blank 0, // protect 0) // acs , 1, self->printer); if (enter_italics_mode && (attributes & ATTRIBUTE_ITALIC)) tputs (enter_italics_mode, 1, self->printer); if (fg >= 0) tputs (g_terminal.color_set_fg[fg], 1, self->printer); if (bg >= 0) tputs (g_terminal.color_set_bg[bg], 1, self->printer); self->dirty = true; } // --- Helpers ----------------------------------------------------------------- static int irc_server_strcmp (struct server *s, const char *a, const char *b) { int x; while (*a || *b) if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++))) return x; return 0; } static int irc_server_strncmp (struct server *s, const char *a, const char *b, size_t n) { int x; while (n-- && (*a || *b)) if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++))) return x; return 0; } static char * irc_cut_nickname (const char *prefix) { return str_cut_until (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) { // This shouldn't be called before successfully registering. // Better safe than sorry, though. if (!s->irc_user) return false; char *nick = irc_cut_nickname (prefix); bool result = !irc_server_strcmp (s, nick, s->irc_user->nickname); free (nick); return result; } static bool irc_is_channel (struct server *s, const char *ident) { return *ident && (!!strchr (s->irc_chantypes, *ident) || !!strchr (s->irc_idchan_prefixes, *ident)); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // 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) { if (!text) return NULL; 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; } // --- 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 (expected to be in UTF-8) // #d inserts a signed integer // // #S inserts a string from the server with unknown encoding // #m inserts a mIRC-formatted string (auto-resets at boundaries) // #n cuts the nickname from a string and automatically colours it // #N is like #n but also appends userhost, if present // // #a inserts named attributes (auto-resets) // #r resets terminal attributes // #c sets foreground color // #C sets background color // // Modifiers: // & free() the string argument after using it static void formatter_add_item (struct formatter *self, struct formatter_item template_) { if (template_.text) template_.text = xstrdup (template_.text); struct formatter_item *item = formatter_item_new (); *item = template_; LIST_APPEND_WITH_TAIL (self->items, self->items_tail, item); } #define FORMATTER_ADD_ITEM(self, type_, ...) formatter_add_item ((self), \ (struct formatter_item) { .type = FORMATTER_ITEM_ ## type_, __VA_ARGS__ }) #define FORMATTER_ADD_RESET(self) \ FORMATTER_ADD_ITEM ((self), ATTR, .attribute = ATTR_RESET) #define FORMATTER_ADD_TEXT(self, text_) \ FORMATTER_ADD_ITEM ((self), TEXT, .text = (text_)) #define FORMATTER_ADD_SIMPLE(self, attribute_) \ FORMATTER_ADD_ITEM ((self), SIMPLE, .attribute = ATTRIBUTE_ ## attribute_) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum { MIRC_WHITE, MIRC_BLACK, MIRC_BLUE, MIRC_GREEN, MIRC_L_RED, MIRC_RED, MIRC_PURPLE, MIRC_ORANGE, MIRC_YELLOW, MIRC_L_GREEN, MIRC_CYAN, MIRC_L_CYAN, MIRC_L_BLUE, MIRC_L_PURPLE, MIRC_GRAY, MIRC_L_GRAY, }; // We use estimates from the 16 color terminal palette, or the 256 color cube, // which is not always available. The mIRC orange colour is only in the cube. static const int g_mirc_to_terminal[] = { [MIRC_WHITE] = COLOR_256 (BRIGHT (WHITE), 231), [MIRC_BLACK] = COLOR_256 (BLACK, 16), [MIRC_BLUE] = COLOR_256 (BLUE, 19), [MIRC_GREEN] = COLOR_256 (GREEN, 34), [MIRC_L_RED] = COLOR_256 (BRIGHT (RED), 196), [MIRC_RED] = COLOR_256 (RED, 124), [MIRC_PURPLE] = COLOR_256 (MAGENTA, 127), [MIRC_ORANGE] = COLOR_256 (BRIGHT (YELLOW), 214), [MIRC_YELLOW] = COLOR_256 (BRIGHT (YELLOW), 226), [MIRC_L_GREEN] = COLOR_256 (BRIGHT (GREEN), 46), [MIRC_CYAN] = COLOR_256 (CYAN, 37), [MIRC_L_CYAN] = COLOR_256 (BRIGHT (CYAN), 51), [MIRC_L_BLUE] = COLOR_256 (BRIGHT (BLUE), 21), [MIRC_L_PURPLE] = COLOR_256 (BRIGHT (MAGENTA),201), [MIRC_GRAY] = COLOR_256 (BRIGHT (BLACK), 244), [MIRC_L_GRAY] = COLOR_256 (WHITE, 252), }; static const char * formatter_parse_mirc_color (struct formatter *self, const char *s) { if (!isdigit_ascii (*s)) return s; int fg = *s++ - '0'; if (isdigit_ascii (*s)) fg = fg * 10 + (*s++ - '0'); if (fg >= 0 && fg < 16) FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]); if (*s != ',' || !isdigit_ascii (s[1])) return s; s++; int bg = *s++ - '0'; if (isdigit_ascii (*s)) bg = bg * 10 + (*s++ - '0'); if (bg >= 0 && bg < 16) FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]); return s; } static void formatter_parse_mirc (struct formatter *self, const char *s) { struct str buf; str_init (&buf); FORMATTER_ADD_RESET (self); unsigned char c; while ((c = *s++)) { if (buf.len && c < 0x20) { FORMATTER_ADD_TEXT (self, buf.str); str_reset (&buf); } switch (c) { case '\x02': FORMATTER_ADD_SIMPLE (self, BOLD); break; case '\x1d': FORMATTER_ADD_SIMPLE (self, ITALIC); break; case '\x1f': FORMATTER_ADD_SIMPLE (self, UNDERLINE); break; case '\x16': FORMATTER_ADD_SIMPLE (self, INVERSE); break; case '\x03': s = formatter_parse_mirc_color (self, s); break; case '\x0f': FORMATTER_ADD_RESET (self); break; default: str_append_c (&buf, c); } } if (buf.len) FORMATTER_ADD_TEXT (self, buf.str); str_free (&buf); FORMATTER_ADD_RESET (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void formatter_parse_nick (struct formatter *self, char *s) { char *nick = irc_cut_nickname (s); int color = str_map_hash (nick, strlen (nick)) % 8; // We always use the default color for ourselves if (self->s && irc_is_this_us (self->s, nick)) color = -1; // Never use the black colour, could become transparent on black terminals if (color == COLOR_BLACK) color = -1; FORMATTER_ADD_ITEM (self, FG_COLOR, .color = color); char *x = irc_to_utf8 (self->ctx, nick); free (nick); FORMATTER_ADD_TEXT (self, x); free (x); // Need to reset the color afterwards FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1); } static void formatter_parse_nick_full (struct formatter *self, char *s) { formatter_parse_nick (self, s); const char *userhost; if (!(userhost = irc_find_userhost (s))) return; FORMATTER_ADD_TEXT (self, " ("); FORMATTER_ADD_ITEM (self, ATTR, .attribute = ATTR_USERHOST); char *x = irc_to_utf8 (self->ctx, userhost); FORMATTER_ADD_TEXT (self, x); free (x); FORMATTER_ADD_RESET (self); FORMATTER_ADD_TEXT (self, ")"); } static const char * formatter_parse_field (struct formatter *self, const char *field, struct str *buf, va_list *ap) { bool free_string = false; char *s = NULL; char *tmp = NULL; int c; restart: switch ((c = *field++)) { // We can push boring text content to the caller's buffer // and let it flush the buffer only when it's actually needed case 'd': tmp = xstrdup_printf ("%d", va_arg (*ap, int)); str_append (buf, tmp); free (tmp); break; case 's': str_append (buf, (s = va_arg (*ap, char *))); break; case 'S': tmp = irc_to_utf8 (self->ctx, (s = va_arg (*ap, char *))); str_append (buf, tmp); free (tmp); break; case 'm': tmp = irc_to_utf8 (self->ctx, (s = va_arg (*ap, char *))); formatter_parse_mirc (self, tmp); free (tmp); break; case 'n': formatter_parse_nick (self, (s = va_arg (*ap, char *))); break; case 'N': formatter_parse_nick_full (self, (s = va_arg (*ap, char *))); break; case 'a': FORMATTER_ADD_ITEM (self, ATTR, .attribute = va_arg (*ap, int)); break; case 'c': FORMATTER_ADD_ITEM (self, FG_COLOR, .color = va_arg (*ap, int)); break; case 'C': FORMATTER_ADD_ITEM (self, BG_COLOR, .color = va_arg (*ap, int)); break; case 'r': FORMATTER_ADD_RESET (self); break; default: if (c == '&' && !free_string) free_string = true; else if (c) hard_assert (!"unexpected format specifier"); else hard_assert (!"unexpected end of format string"); goto restart; } if (free_string) free (s); return field; } // I was unable to take a pointer of a bare "va_list" when it was passed in // as a function argument, so it has to be a pointer from the beginning static void formatter_addv (struct formatter *self, const char *format, va_list *ap) { struct str buf; str_init (&buf); 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); } static void formatter_add (struct formatter *self, const char *format, ...) { va_list ap; va_start (ap, format); formatter_addv (self, format, &ap); va_end (ap); } static void formatter_add_from (struct formatter *self, struct formatter *other) { for (struct formatter_item *iter = other->items; iter; iter = iter->next) formatter_add_item (self, *iter); } static bool formatter_flush_attr (struct attribute_printer *state, struct formatter_item *item) { switch (item->type) { case FORMATTER_ITEM_ATTR: attribute_printer_apply (state, item->attribute); state->want = 0; state->want_foreground = -1; state->want_background = -1; return true; case FORMATTER_ITEM_SIMPLE: state->want |= item->attribute; attribute_printer_update (state); return true; case FORMATTER_ITEM_FG_COLOR: state->want_foreground = item->color; attribute_printer_update (state); return true; case FORMATTER_ITEM_BG_COLOR: state->want_background = item->color; attribute_printer_update (state); return true; default: return false; } } static void formatter_flush_text (struct app_context *ctx, const char *text, FILE *stream) { struct str sanitized; str_init (&sanitized); // Throw away any potentially harmful control characters char *term = iconv_xstrdup (ctx->term_from_utf8, (char *) text, -1, NULL); for (char *p = term; *p; p++) if (!strchr ("\a\b\x1b", *p)) str_append_c (&sanitized, *p); free (term); fputs (sanitized.str, stream); str_free (&sanitized); } 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; } struct attribute_printer state; attribute_printer_init (&state, self->ctx, printer); attribute_printer_reset (&state); int attribute_ignore = 0; LIST_FOR_EACH (struct formatter_item, iter, self->items) { if (iter->type == FORMATTER_ITEM_TEXT) formatter_flush_text (self->ctx, iter->text, stream); else if (iter->type == FORMATTER_ITEM_IGNORE_ATTR) attribute_ignore += iter->attribute; else if (attribute_ignore <= 0 && !formatter_flush_attr (&state, iter)) hard_assert (!"unhandled formatter item type"); } attribute_printer_reset (&state); } // --- 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_flush (struct buffer_line *line, struct formatter *f, FILE *output) { int flags = line->flags; if (flags & BUFFER_LINE_INDENT) formatter_add (f, " "); if (flags & BUFFER_LINE_STATUS) formatter_add (f, " - "); if (flags & BUFFER_LINE_ERROR) formatter_add (f, "#a=!=#r ", ATTR_ERROR); formatter_add_from (f, line->formatter); formatter_add (f, "\n"); formatter_flush (f, output); formatter_free (f); } 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 formatter f; formatter_init (&f, ctx, NULL); struct tm current; char buf[9]; if (!localtime_r (&line->when, ¤t)) print_error ("%s: %s", "localtime_r", strerror (errno)); else if (!strftime (buf, sizeof buf, "%T", ¤t)) print_error ("%s: %s", "strftime", "buffer too small"); else formatter_add (&f, "#a#s#r ", ATTR_TIMESTAMP, buf); // 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); FORMATTER_ADD_ITEM (&f, IGNORE_ATTR, .attribute = 1); } input_hide (&ctx->input); buffer_line_flush (line, &f, stdout); input_show (&ctx->input); } static void buffer_line_write_to_log (struct app_context *ctx, struct buffer_line *line, FILE *log_file) { if (line->flags & BUFFER_LINE_SKIP_FILE) return; struct formatter f; formatter_init (&f, ctx, NULL); struct tm current; char buf[20]; if (!gmtime_r (&line->when, ¤t)) print_error ("%s: %s", "gmtime_r", strerror (errno)); else if (!strftime (buf, sizeof buf, "%F %T", ¤t)) print_error ("%s: %s", "strftime", "buffer too small"); else formatter_add (&f, "#s ", buf); buffer_line_flush (line, &f, log_file); } static void log_formatter (struct app_context *ctx, struct buffer *buffer, int flags, struct formatter *f) { if (!buffer) buffer = ctx->global_buffer; struct buffer_line *line = buffer_line_new (); line->flags = flags; line->when = time (NULL); // Move the formatter inside line->formatter = xmalloc (sizeof *line->formatter); *line->formatter = *f; LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line); buffer->lines_count++; if (buffer->log_file) buffer_line_write_to_log (ctx, line, buffer->log_file); if (ctx->beep_on_highlight) if ((flags & BUFFER_LINE_HIGHLIGHT) || (buffer->type == BUFFER_PM && buffer != ctx->current_buffer)) input_ding (&ctx->input); 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 (buffer == ctx->current_buffer) buffer_line_display (ctx, line, false); else if (!ctx->isolate_buffers && can_leak) buffer_line_display (ctx, line, true); else { buffer->unseen_messages_count++; if (flags & BUFFER_LINE_HIGHLIGHT) buffer->highlighted = true; refresh_prompt (ctx); } } static void log_full (struct app_context *ctx, struct server *s, struct buffer *buffer, int flags, const char *format, ...) { va_list ap; va_start (ap, format); struct formatter f; formatter_init (&f, ctx, s); formatter_addv (&f, format, &ap); log_formatter (ctx, buffer, flags, &f); va_end (ap); } #define log_global(ctx, flags, ...) \ log_full ((ctx), NULL, (ctx)->global_buffer, flags, __VA_ARGS__) #define log_server(s, buffer, flags, ...) \ log_full ((s)->ctx, s, (buffer), flags, __VA_ARGS__) #define log_global_status(ctx, ...) \ log_global ((ctx), BUFFER_LINE_STATUS, __VA_ARGS__) #define log_global_error(ctx, ...) \ log_global ((ctx), BUFFER_LINE_ERROR, __VA_ARGS__) #define log_global_indent(ctx, ...) \ log_global ((ctx), BUFFER_LINE_INDENT, __VA_ARGS__) #define log_server_status(s, buffer, ...) \ log_server ((s), (buffer), BUFFER_LINE_STATUS, __VA_ARGS__) #define log_server_error(s, buffer, ...) \ log_server ((s), (buffer), BUFFER_LINE_ERROR, __VA_ARGS__) #define log_global_debug(ctx, ...) \ BLOCK_START \ if (g_debug_mode) \ log_global ((ctx), 0, "(*) " __VA_ARGS__); \ BLOCK_END #define log_server_debug(s, ...) \ BLOCK_START \ if (g_debug_mode) \ log_server ((s), (s)->buffer, 0, "(*) " __VA_ARGS__); \ BLOCK_END // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Lines that are used in more than one place #define log_nick_self(s, buffer, new_) \ log_server_status ((s), (buffer), "You are now known as #n", (new_)) #define log_nick(s, buffer, old, new_) \ log_server_status ((s), (buffer), "#n is now known as #n", (old), (new_)) #define log_outcoming_notice(s, buffer, who, text) \ log_server_status ((s), (buffer), "#s(#n): #m", "Notice", (who), (text)) #define log_outcoming_privmsg(s, buffer, prefixes, who, text) \ log_server ((s), (buffer), 0, "<#s#n> #m", (prefixes), (who), (text)) #define log_outcoming_action(s, buffer, who, text) \ log_server ((s), (buffer), 0, " #a*#r #n #m", ATTR_ACTION, (who), (text)) #define log_outcoming_orphan_notice(s, target, text) \ log_server_status ((s), (s)->buffer, "Notice -> #n: #m", (target), (text)) #define log_outcoming_orphan_privmsg(s, target, text) \ log_server_status ((s), (s)->buffer, "MSG(#n): #m", (target), (text)) #define log_ctcp_query(s, target, tag) \ log_server_status ((s), (s)->buffer, "CTCP query to #S: #S", target, tag) #define log_ctcp_reply(s, target, reply /* freed! */) \ log_server_status ((s), (s)->buffer, "CTCP reply to #S: #&S", target, reply) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void make_log_filename (const char *filename, struct str *output) { for (const char *p = filename; *p; p++) // XXX: anything more to replace? if (strchr ("/\\ ", *p)) str_append_c (output, '_'); else str_append_c (output, tolower_ascii (*p)); } static void buffer_open_log_file (struct app_context *ctx, struct buffer *buffer) { if (!ctx->logging || buffer->log_file) return; struct str path; str_init (&path); get_xdg_home_dir (&path, "XDG_DATA_HOME", ".local/share"); str_append_printf (&path, "/%s/%s", PROGRAM_NAME, "logs"); (void) mkdir_with_parents (path.str, NULL); // TODO: make sure global and server buffers don't collide with filenames str_append_c (&path, '/'); make_log_filename (buffer->name, &path); str_append (&path, ".log"); if (!(buffer->log_file = fopen (path.str, "ab"))) log_global_error (ctx, "Couldn't open log file `#s': #s", path.str, strerror (errno)); str_free (&path); } static void buffer_close_log_file (struct buffer *buffer) { if (buffer->log_file) (void) fclose (buffer->log_file); buffer->log_file = NULL; } static void on_config_logging_change (struct config_item_ *item) { struct app_context *ctx = item->user_data; ctx->logging = item->value.boolean; for (struct buffer *buffer = ctx->buffers; buffer; buffer = buffer->next) if (ctx->logging) buffer_open_log_file (ctx, buffer); else buffer_close_log_file (buffer); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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); buffer_open_log_file (ctx, 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); hard_assert (buffer != ctx->global_buffer); hard_assert (buffer->type != BUFFER_SERVER); 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); if (buffer == ctx->last_buffer) ctx->last_buffer = NULL; str_map_set (&ctx->buffers_by_name, buffer->name, NULL); LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer); buffer_destroy (buffer); refresh_prompt (ctx); } static void buffer_print_read_marker (struct app_context *ctx) { struct formatter f; formatter_init (&f, ctx, NULL); formatter_add (&f, "#a-- -- -- ---\n", ATTR_READ_MARKER); formatter_flush (&f, stdout); formatter_free (&f); } static void buffer_print_backlog (struct app_context *ctx, struct buffer *buffer) { // The prompt can take considerable time to redraw input_hide (&ctx->input); print_status ("%s", buffer->name); // That is, minus the buffer switch line and the readline prompt int display_limit = MAX (MAX (10, buffer->unseen_messages_count), g_terminal.lines - 2); struct buffer_line *line = buffer->lines_tail; int to_display = line != NULL; for (; line && line->prev && --display_limit > 0; line = line->prev) to_display++; // Once we've found where we want to start with the backlog, print it int until_marker = to_display - (int) buffer->unseen_messages_count; for (; line; line = line->next) { if (until_marker-- == 0) buffer_print_read_marker (ctx); buffer_line_display (ctx, line, false); } buffer->unseen_messages_count = 0; buffer->highlighted = false; // So that it is obvious if the last line in the buffer is not from today buffer_update_time (ctx, time (NULL)); refresh_prompt (ctx); input_show (&ctx->input); } static void buffer_activate (struct app_context *ctx, struct buffer *buffer) { if (ctx->current_buffer == buffer) return; buffer_print_backlog (ctx, buffer); 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) { // XXX: anything better to do? This situation is arguably rare and I'm // not entirely sure what action to take. log_full (ctx, NULL, buffer, BUFFER_LINE_STATUS, "Buffer #s was merged into this buffer", merged->name); // Find all lines from "merged" newer than the newest line in "buffer" struct buffer_line *start = merged->lines; if (buffer->lines_tail) while (start && start->when < buffer->lines_tail->when) start = start->next; if (!start) return; // Count how many of them we have size_t n = 0; for (struct buffer_line *iter = start; iter; iter = iter->next) n++; struct buffer_line *tail = merged->lines_tail; // Cut them from the original buffer if (start == merged->lines) merged->lines = NULL; else if (start->prev) start->prev->next = NULL; if (start == merged->lines_tail) merged->lines_tail = start->prev; merged->lines_count -= n; // And append them to current lines in the buffer buffer->lines_tail->next = start; start->prev = buffer->lines_tail; buffer->lines_tail = tail; buffer->lines_count += n; log_full (ctx, NULL, buffer, BUFFER_LINE_STATUS | BUFFER_LINE_SKIP_FILE, "End of merged content"); } static void buffer_rename (struct app_context *ctx, struct buffer *buffer, const char *new_name) { struct buffer *collision = str_map_find (&ctx->buffers_by_name, new_name); if (collision == buffer) return; hard_assert (!collision); str_map_set (&ctx->buffers_by_name, buffer->name, NULL); str_map_set (&ctx->buffers_by_name, new_name, buffer); buffer_close_log_file (buffer); buffer_open_log_file (ctx, buffer); free (buffer->name); buffer->name = xstrdup (new_name); // We might have renamed the current buffer refresh_prompt (ctx); } static void buffer_clear (struct buffer *buffer) { LIST_FOR_EACH (struct buffer_line, iter, buffer->lines) buffer_line_destroy (iter); buffer->lines = buffer->lines_tail = NULL; buffer->lines_count = 0; } 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_global_buffer (struct app_context *ctx) { struct buffer *global = ctx->global_buffer = buffer_new (); global->type = BUFFER_GLOBAL; global->name = xstrdup (PROGRAM_NAME); buffer_add (ctx, global); buffer_activate (ctx, global); } // --- Users, channels --------------------------------------------------------- static void irc_user_on_destroy (void *object, void *user_data) { struct user *user = object; struct server *s = user_data; if (!s->rehashing) 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; } struct user * irc_get_or_make_user (struct server *s, const char *nickname) { struct user *user = str_map_find (&s->irc_users, nickname); if (user) return user_ref (user); return irc_make_user (s, xstrdup (nickname)); } 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 = irc_get_or_make_user (s, nickname); // Open a new buffer for the user buffer = buffer_new (); buffer->type = BUFFER_PM; buffer->name = xstrdup_printf ("%s.%s", s->name, nickname); buffer->server = s; buffer->user = user; str_map_set (&s->irc_buffer_map, user->nickname, buffer); buffer_add (s->ctx, buffer); return buffer; } // Note that this eats the user reference static void irc_channel_link_user (struct channel *channel, struct user *user, const char *prefixes) { 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; str_append (&channel_user->prefixes, prefixes); LIST_PREPEND (channel->users, channel_user); } 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); if (!s->rehashing) 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->topic = NULL; str_map_set (&s->irc_channels, channel->name, channel); return channel; } static struct channel_user * irc_channel_get_user (struct channel *channel, struct user *user) { LIST_FOR_EACH (struct channel_user, iter, channel->users) if (iter->user == user) return iter; return NULL; } static void irc_remove_user_from_channel (struct user *user, struct channel *channel) { struct channel_user *channel_user = irc_channel_get_user (channel, user); if (channel_user) irc_channel_unlink_user (channel, channel_user); } static void irc_left_channel (struct channel *channel) { LIST_FOR_EACH (struct channel_user, iter, channel->users) irc_channel_unlink_user (channel, iter); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void remove_conflicting_buffer (struct server *s, struct buffer *buffer) { log_server_status (s, s->buffer, "Removed buffer #s because of casemapping conflict", buffer->name); if (s->ctx->current_buffer == buffer) buffer_activate (s->ctx, s->buffer); buffer_remove (s->ctx, buffer); } static void irc_try_readd_user (struct server *s, struct user *user, struct buffer *buffer) { if (str_map_find (&s->irc_users, user->nickname)) { // Remove user from all channels and destroy any PM buffer user_ref (user); LIST_FOR_EACH (struct user_channel, iter, user->channels) irc_remove_user_from_channel (user, iter->channel); if (buffer) remove_conflicting_buffer (s, buffer); user_unref (user); } else { str_map_set (&s->irc_users, user->nickname, user); str_map_set (&s->irc_buffer_map, user->nickname, buffer); } } static void irc_try_readd_channel (struct server *s, struct channel *channel, struct buffer *buffer) { if (str_map_find (&s->irc_channels, channel->name)) { // Remove all users from channel and destroy any channel buffer channel_ref (channel); LIST_FOR_EACH (struct channel_user, iter, channel->users) irc_channel_unlink_user (channel, iter); if (buffer) remove_conflicting_buffer (s, buffer); channel_unref (channel); } else { str_map_set (&s->irc_channels, channel->name, channel); str_map_set (&s->irc_buffer_map, channel->name, buffer); } } static void irc_rehash_and_fix_conflicts (struct server *s) { // Save the old maps and initialize new ones struct str_map old_users = s->irc_users; struct str_map old_channels = s->irc_channels; struct str_map old_buffer_map = s->irc_buffer_map; str_map_init (&s->irc_users); str_map_init (&s->irc_channels); str_map_init (&s->irc_buffer_map); s->irc_users .key_xfrm = s->irc_strxfrm; s->irc_channels .key_xfrm = s->irc_strxfrm; s->irc_buffer_map.key_xfrm = s->irc_strxfrm; // Prevent channels and users from unsetting themselves // from server maps upon removing the last reference to them s->rehashing = true; // XXX: to be perfectly sure, we should also check // whether any users collide with channels and vice versa // Our own user always takes priority, add him first if (s->irc_user) irc_try_readd_user (s, s->irc_user, str_map_find (&old_buffer_map, s->irc_user->nickname)); struct str_map_iter iter; struct user *user; struct channel *channel; str_map_iter_init (&iter, &old_users); while ((user = str_map_iter_next (&iter))) irc_try_readd_user (s, user, str_map_find (&old_buffer_map, user->nickname)); str_map_iter_init (&iter, &old_channels); while ((channel = str_map_iter_next (&iter))) irc_try_readd_channel (s, channel, str_map_find (&old_buffer_map, channel->name)); // Hopefully we've either moved or destroyed all the old content s->rehashing = false; str_map_free (&old_users); str_map_free (&old_channels); str_map_free (&old_buffer_map); } static void irc_set_casemapping (struct server *s, irc_tolower_fn tolower, irc_strxfrm_fn strxfrm) { if (tolower == s->irc_tolower && strxfrm == s->irc_strxfrm) return; s->irc_tolower = tolower; s->irc_strxfrm = strxfrm; // Ideally we would never have to do this but I can't think of a workaround irc_rehash_and_fix_conflicts (s); } // --- Core functionality ------------------------------------------------------ static bool irc_is_connected (struct server *s) { return s->state != IRC_DISCONNECTED && s->state != IRC_CONNECTING; } static void irc_update_poller (struct server *s, const struct pollfd *pfd) { int new_events = s->transport->get_poll_events (s); hard_assert (new_events != 0); if (!pfd || pfd->events != new_events) poller_fd_set (&s->socket_event, new_events); } static void irc_cancel_timers (struct server *s) { poller_timer_reset (&s->timeout_tmr); poller_timer_reset (&s->ping_tmr); poller_timer_reset (&s->reconnect_tmr); } static void irc_reset_connection_timeouts (struct server *s) { irc_cancel_timers (s); poller_timer_set (&s->timeout_tmr, 3 * 60 * 1000); poller_timer_set (&s->ping_tmr, (3 * 60 + 30) * 1000); } static void irc_queue_reconnect (struct server *s) { // As long as the user wants us to, that is if (!get_config_boolean (s->config, "reconnect")) return; int64_t delay = get_config_integer (s->config, "reconnect_delay"); // TODO: exponentional backoff // XXX: maybe add a state for when a connect is queued? hard_assert (s->state == IRC_DISCONNECTED); log_server_status (s, s->buffer, "Trying to reconnect in #&s seconds...", xstrdup_printf ("%" PRId64, delay)); poller_timer_set (&s->reconnect_tmr, delay * 1000); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void irc_send (struct server *s, const char *format, ...) ATTRIBUTE_PRINTF (2, 3); static void irc_send (struct server *s, const char *format, ...) { if (!soft_assert (irc_is_connected (s))) { log_server_debug (s, "sending a message to a dead server connection"); return; } if (s->state == IRC_CLOSING || s->state == IRC_HALF_CLOSED) return; va_list ap; va_start (ap, format); struct str str; str_init (&str); str_append_vprintf (&str, format, ap); va_end (ap); log_server_debug (s, "#a<< \"#S\"#r", ATTR_PART, str.str); str_append_str (&s->write_buffer, &str); str_free (&str); str_append (&s->write_buffer, "\r\n"); irc_update_poller (s, NULL); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void irc_real_shutdown (struct server *s) { hard_assert (irc_is_connected (s) && s->state != IRC_HALF_CLOSED); if (s->transport && s->transport->in_before_shutdown) s->transport->in_before_shutdown (s); while (shutdown (s->socket, SHUT_WR) == -1) if (!soft_assert (errno == EINTR)) break; s->state = IRC_HALF_CLOSED; } static void irc_shutdown (struct server *s) { if (s->state == IRC_CLOSING || s->state == IRC_HALF_CLOSED) return; // TODO: set a timer to cut the connection if we don't receive an EOF s->state = IRC_CLOSING; // Either there's still some data in the write buffer and we wait // until they're sent, or we send an EOF to the server right away if (!s->write_buffer.len) irc_real_shutdown (s); } static void irc_destroy_connector (struct server *s) { connector_free (s->connector); free (s->connector); s->connector = NULL; // Not connecting anymore s->state = IRC_DISCONNECTED; } static void try_finish_quit (struct app_context *ctx) { if (!ctx->quitting) return; struct str_map_iter iter; str_map_iter_init (&iter, &ctx->servers); bool disconnected_all = true; struct server *s; while ((s = str_map_iter_next (&iter))) if (irc_is_connected (s)) disconnected_all = false; if (disconnected_all) ctx->polling = false; } static void initiate_quit (struct app_context *ctx) { log_global_status (ctx, "Shutting down"); // Destroy the user interface input_stop (&ctx->input); // Initiate a connection close struct str_map_iter iter; str_map_iter_init (&iter, &ctx->servers); struct server *s; while ((s = str_map_iter_next (&iter))) { // There may be a timer set to reconnect to the server poller_timer_reset (&s->reconnect_tmr); if (irc_is_connected (s)) { irc_shutdown (s); s->manual_disconnect = true; } else if (s->state == IRC_CONNECTING) irc_destroy_connector (s); } ctx->quitting = true; try_finish_quit (ctx); } static void irc_destroy_transport (struct server *s) { if (s->transport && s->transport->cleanup) s->transport->cleanup (s); s->transport = NULL; xclose (s->socket); s->socket = -1; s->state = IRC_DISCONNECTED; s->socket_event.closed = true; poller_fd_reset (&s->socket_event); str_reset (&s->read_buffer); str_reset (&s->write_buffer); } static void irc_destroy_state (struct server *s) { struct str_map_iter iter; str_map_iter_init (&iter, &s->irc_channels); struct channel *channel; while ((channel = str_map_iter_next (&iter))) irc_left_channel (channel); if (s->irc_user) { user_unref (s->irc_user); s->irc_user = NULL; } str_reset (&s->irc_user_mode); free (s->irc_user_host); s->irc_user_host = NULL; s->cap_echo_message = false; // Need to call this before server_init_specifics() irc_set_casemapping (s, irc_tolower, irc_strxfrm); server_free_specifics (s); server_init_specifics (s); } static void irc_disconnect (struct server *s) { hard_assert (irc_is_connected (s)); struct str_map_iter iter; str_map_iter_init (&iter, &s->irc_buffer_map); struct buffer *buffer; while ((buffer = str_map_iter_next (&iter))) log_server_status (s, buffer, "Disconnected from server"); irc_cancel_timers (s); irc_destroy_transport (s); irc_destroy_state (s); // Take any relevant actions if (s->ctx->quitting) try_finish_quit (s->ctx); else if (s->manual_disconnect) s->manual_disconnect = false; else irc_queue_reconnect (s); refresh_prompt (s->ctx); } static void irc_initiate_disconnect (struct server *s, const char *reason) { hard_assert (irc_is_connected (s)); s->manual_disconnect = true; if (reason) irc_send (s, "QUIT :%s", reason); else irc_send (s, "QUIT :%s", PROGRAM_NAME " " PROGRAM_VERSION); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void on_irc_ping_timeout (void *user_data) { struct server *s = user_data; log_server_error (s, s->buffer, "Connection timeout"); irc_disconnect (s); } static void on_irc_timeout (void *user_data) { // Provoke a response from the server struct server *s = user_data; irc_send (s, "PING :%" PRIi64, (int64_t) time (NULL)); } // --- Server I/O -------------------------------------------------------------- static void irc_process_message (const struct irc_message *msg, const char *raw, void *user_data); static enum transport_io_result irc_try_read (struct server *s) { enum transport_io_result result = s->transport->try_read (s); if (result == TRANSPORT_IO_OK) { if (s->read_buffer.len >= (1 << 20)) { // XXX: this is stupid; if anything, count it in dependence of time log_server_error (s, s->buffer, "The IRC server seems to spew out data frantically"); return TRANSPORT_IO_ERROR; } if (s->read_buffer.len) irc_process_buffer (&s->read_buffer, irc_process_message, s); } return result; } static enum transport_io_result irc_try_write (struct server *s) { enum transport_io_result result = s->transport->try_write (s); if (result == TRANSPORT_IO_OK) { // If we're flushing the write buffer and our job is complete, we send // an EOF to the server, changing the state to IRC_HALF_CLOSED if (s->state == IRC_CLOSING && !s->write_buffer.len) irc_real_shutdown (s); } return result; } static bool irc_try_read_write (struct server *s) { enum transport_io_result read_result; enum transport_io_result write_result; if ((read_result = irc_try_read (s)) == TRANSPORT_IO_ERROR || (write_result = irc_try_write (s)) == TRANSPORT_IO_ERROR) { log_server_error (s, s->buffer, "Server connection failed"); return false; } // FIXME: this may probably fire multiple times when we're flushing, // we should probably store a flag next to the state if (read_result == TRANSPORT_IO_EOF || write_result == TRANSPORT_IO_EOF) log_server_error (s, s->buffer, "Server closed the connection"); // If the write needs to read and we receive an EOF, we can't flush if (write_result == TRANSPORT_IO_EOF) return false; if (read_result == TRANSPORT_IO_EOF) { // Eventually initiate shutdown to flush the write buffer irc_shutdown (s); // If there's nothing to write, we can disconnect now if (s->state == IRC_HALF_CLOSED) return false; } return true; } static void on_irc_ready (const struct pollfd *pfd, struct server *s) { if (irc_try_read_write (s)) { // XXX: shouldn't we rather wait for PONG messages? irc_reset_connection_timeouts (s); irc_update_poller (s, pfd); } else // We don't want to keep the socket anymore irc_disconnect (s); } // --- Plain transport --------------------------------------------------------- static enum transport_io_result transport_plain_try_read (struct server *s) { struct str *buf = &s->read_buffer; ssize_t n_read; while (true) { str_ensure_space (buf, 512); n_read = recv (s->socket, buf->str + buf->len, buf->alloc - buf->len - 1 /* null byte */, 0); if (n_read > 0) { buf->str[buf->len += n_read] = '\0'; continue; } if (n_read == 0) return TRANSPORT_IO_EOF; if (errno == EAGAIN) return TRANSPORT_IO_OK; if (errno == EINTR) continue; LOG_LIBC_FAILURE ("recv"); return TRANSPORT_IO_ERROR; } } static enum transport_io_result transport_plain_try_write (struct server *s) { struct str *buf = &s->write_buffer; ssize_t n_written; while (buf->len) { n_written = send (s->socket, buf->str, buf->len, 0); if (n_written >= 0) { str_remove_slice (buf, 0, n_written); continue; } if (errno == EAGAIN) return TRANSPORT_IO_OK; if (errno == EINTR) continue; LOG_LIBC_FAILURE ("send"); return TRANSPORT_IO_ERROR; } return TRANSPORT_IO_OK; } static int transport_plain_get_poll_events (struct server *s) { int events = POLLIN; if (s->write_buffer.len) events |= POLLOUT; return events; } static struct transport g_transport_plain = { .try_read = transport_plain_try_read, .try_write = transport_plain_try_write, .get_poll_events = transport_plain_get_poll_events, }; // --- SSL/TLS transport ------------------------------------------------------- struct transport_tls_data { SSL_CTX *ssl_ctx; ///< SSL context SSL *ssl; ///< SSL/TLS connection bool ssl_rx_want_tx; ///< SSL_read() wants to write bool ssl_tx_want_rx; ///< SSL_write() wants to read }; static bool transport_tls_init_ctx (struct server *s, SSL_CTX *ssl_ctx, struct error **e) { bool verify = get_config_boolean (s->config, "ssl_verify"); if (!verify) SSL_CTX_set_verify (ssl_ctx, SSL_VERIFY_NONE, NULL); const char *ca_file = get_config_string (s->config, "ssl_ca_file"); const char *ca_path = get_config_string (s->config, "ssl_ca_path"); struct error *error = NULL; if (ca_file || ca_path) { if (SSL_CTX_load_verify_locations (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 (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; } // TODO: allow specifying SSL_CTX_set_cipher_list() SSL_CTX_set_mode (ssl_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); return true; ca_error: if (verify) { error_propagate (e, error); return false; } // Only inform the user if we're not actually verifying log_server_error (s, s->buffer, "#s", error->message); error_free (error); return true; } static bool transport_tls_init_cert (struct server *s, SSL *ssl, struct error **e) { const char *ssl_cert = get_config_string (s->config, "ssl_cert"); if (!ssl_cert) return true; bool result = false; char *path = resolve_config_filename (ssl_cert); if (!path) error_set (e, "%s: %s", "Cannot open file", ssl_cert); // XXX: perhaps we should read the file ourselves for better messages else if (!SSL_use_certificate_file (ssl, path, SSL_FILETYPE_PEM) || !SSL_use_PrivateKey_file (ssl, path, SSL_FILETYPE_PEM)) error_set (e, "%s: %s", "Setting the SSL client certificate failed", ERR_error_string (ERR_get_error (), NULL)); else result = true; free (path); return result; } static bool transport_tls_init (struct server *s, struct error **e) { const char *error_info = NULL; SSL_CTX *ssl_ctx = SSL_CTX_new (SSLv23_client_method ()); if (!ssl_ctx) goto error_ssl_1; if (!transport_tls_init_ctx (s, ssl_ctx, e)) goto error_ssl_2; SSL *ssl = SSL_new (ssl_ctx); if (!ssl) goto error_ssl_2; struct error *error = NULL; if (!transport_tls_init_cert (s, ssl, &error)) { // XXX: is this a reason to abort the connection? log_server_error (s, s->buffer, "#s", error->message); error_free (error); } SSL_set_connect_state (ssl); if (!SSL_set_fd (ssl, s->socket)) goto error_ssl_3; // XXX: maybe set `ssl_rx_want_tx' to force a handshake? struct transport_tls_data *data = xcalloc (1, sizeof *data); data->ssl_ctx = ssl_ctx; data->ssl = ssl; s->transport_data = data; return true; error_ssl_3: SSL_free (ssl); error_ssl_2: SSL_CTX_free (ssl_ctx); 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/TLS", error_info); return false; } static void transport_tls_cleanup (struct server *s) { struct transport_tls_data *data = s->transport_data; if (data->ssl) SSL_free (data->ssl); if (data->ssl_ctx) SSL_CTX_free (data->ssl_ctx); free (data); } static enum transport_io_result transport_tls_try_read (struct server *s) { struct transport_tls_data *data = s->transport_data; if (data->ssl_tx_want_rx) return TRANSPORT_IO_OK; struct str *buf = &s->read_buffer; data->ssl_rx_want_tx = false; while (true) { str_ensure_space (buf, 512); int n_read = SSL_read (data->ssl, buf->str + buf->len, buf->alloc - buf->len - 1 /* null byte */); const char *error_info = NULL; switch (xssl_get_error (data->ssl, n_read, &error_info)) { case SSL_ERROR_NONE: buf->str[buf->len += n_read] = '\0'; continue; case SSL_ERROR_ZERO_RETURN: return TRANSPORT_IO_EOF; case SSL_ERROR_WANT_READ: return TRANSPORT_IO_OK; case SSL_ERROR_WANT_WRITE: data->ssl_rx_want_tx = true; return TRANSPORT_IO_OK; case XSSL_ERROR_TRY_AGAIN: continue; default: LOG_FUNC_FAILURE ("SSL_read", error_info); return TRANSPORT_IO_ERROR; } } } static enum transport_io_result transport_tls_try_write (struct server *s) { struct transport_tls_data *data = s->transport_data; if (data->ssl_rx_want_tx) return TRANSPORT_IO_OK; struct str *buf = &s->write_buffer; data->ssl_tx_want_rx = false; while (buf->len) { int n_written = SSL_write (data->ssl, buf->str, buf->len); const char *error_info = NULL; switch (xssl_get_error (data->ssl, n_written, &error_info)) { case SSL_ERROR_NONE: str_remove_slice (buf, 0, n_written); continue; case SSL_ERROR_ZERO_RETURN: return TRANSPORT_IO_EOF; case SSL_ERROR_WANT_WRITE: return TRANSPORT_IO_OK; case SSL_ERROR_WANT_READ: data->ssl_tx_want_rx = true; return TRANSPORT_IO_OK; case XSSL_ERROR_TRY_AGAIN: continue; default: LOG_FUNC_FAILURE ("SSL_write", error_info); return TRANSPORT_IO_ERROR; } } return TRANSPORT_IO_OK; } static int transport_tls_get_poll_events (struct server *s) { struct transport_tls_data *data = s->transport_data; int events = POLLIN; if (s->write_buffer.len || data->ssl_rx_want_tx) events |= POLLOUT; // While we're waiting for an opposite event, we ignore the original if (data->ssl_rx_want_tx) events &= ~POLLIN; if (data->ssl_tx_want_rx) events &= ~POLLOUT; return events; } static void transport_tls_in_before_shutdown (struct server *s) { struct transport_tls_data *data = s->transport_data; (void) SSL_shutdown (data->ssl); } static struct transport g_transport_tls = { .init = transport_tls_init, .cleanup = transport_tls_cleanup, .try_read = transport_tls_try_read, .try_write = transport_tls_try_write, .get_poll_events = transport_tls_get_poll_events, .in_before_shutdown = transport_tls_in_before_shutdown, }; // --- Connection establishment ------------------------------------------------ static bool irc_autofill_user_info (struct server *s, struct error **e) { const char *nicks = get_config_string (s->config, "nicks"); const char *username = get_config_string (s->config, "username"); const char *realname = get_config_string (s->config, "realname"); if (nicks && *nicks && username && *username && realname) return true; // Read POSIX user info and fill the configuration if needed struct passwd *pwd = getpwuid (geteuid ()); if (!pwd) FAIL ("cannot retrieve user information: %s", strerror (errno)); // FIXME: set_config_strings() writes errors on its own if (!nicks || !*nicks) set_config_string (s->config, "nicks", pwd->pw_name); if (!username || !*username) set_config_string (s->config, "username", pwd->pw_name); // Not all systems have the GECOS field but the vast majority does if (!realname) { char *gecos = pwd->pw_gecos; // The first comma, if any, ends the user's real name char *comma = strchr (gecos, ','); if (comma) *comma = '\0'; set_config_string (s->config, "realname", gecos); } return true; } static char * irc_fetch_next_nickname (struct server *s) { struct str_vector v; str_vector_init (&v); split_str_ignore_empty (get_config_string (s->config, "nicks"), ',', &v); char *result = NULL; if (s->nick_counter >= 0 && (size_t) s->nick_counter < v.len) result = str_vector_steal (&v, s->nick_counter++); if ((size_t) s->nick_counter >= v.len) // Exhausted all nicknames s->nick_counter = -1; str_vector_free (&v); return result; } static void irc_register (struct server *s) { // Fill in user information automatically if needed irc_autofill_user_info (s, NULL); const char *username = get_config_string (s->config, "username"); const char *realname = get_config_string (s->config, "realname"); hard_assert (username && realname); // Start IRCv3.1 capability negotiation; // at worst the server will ignore this or send a harmless error message irc_send (s, "CAP LS"); const char *password = get_config_string (s->config, "password"); if (password) irc_send (s, "PASS :%s", password); s->nick_counter = 0; char *nickname = irc_fetch_next_nickname (s); if (nickname) irc_send (s, "NICK :%s", nickname); else log_server_error (s, s->buffer, "No nicks present in configuration"); free (nickname); // IRC servers may ignore the last argument if it's empty irc_send (s, "USER %s 8 * :%s", username, *realname ? realname : " "); } static void irc_finish_connection (struct server *s, int socket) { struct app_context *ctx = s->ctx; set_blocking (socket, false); s->socket = socket; s->transport = get_config_boolean (s->config, "ssl") ? &g_transport_tls : &g_transport_plain; struct error *e = NULL; if (s->transport->init && !s->transport->init (s, &e)) { log_server_error (s, s->buffer, "Connection failed: #s", e->message); error_free (e); xclose (s->socket); s->socket = -1; s->transport = NULL; return; } log_server_status (s, s->buffer, "Connection established"); s->state = IRC_CONNECTED; poller_fd_init (&s->socket_event, &ctx->poller, s->socket); s->socket_event.dispatcher = (poller_fd_fn) on_irc_ready; s->socket_event.user_data = s; irc_update_poller (s, NULL); irc_reset_connection_timeouts (s); irc_register (s); refresh_prompt (s->ctx); } static void irc_on_connector_connecting (void *user_data, const char *address) { struct server *s = user_data; log_server_status (s, s->buffer, "Connecting to #s...", address); } static void irc_on_connector_error (void *user_data, const char *error) { struct server *s = user_data; log_server_error (s, s->buffer, "Connection failed: #s", error); } static void irc_on_connector_failure (void *user_data) { struct server *s = user_data; irc_destroy_connector (s); irc_queue_reconnect (s); } static void irc_on_connector_connected (void *user_data, int socket) { struct server *s = user_data; irc_destroy_connector (s); irc_finish_connection (s, socket); } static void irc_split_host_port (char *s, char **host, char **port) { char *colon = strchr (s, ':'); if (colon) { *colon = '\0'; *port = ++colon; } else *port = "6667"; *host = s; } static bool irc_setup_connector (struct server *s, const struct str_vector *addresses, struct error **e) { struct connector *connector = xmalloc (sizeof *connector); connector_init (connector, &s->ctx->poller); connector->user_data = s; connector->on_connecting = irc_on_connector_connecting; connector->on_error = irc_on_connector_error; connector->on_connected = irc_on_connector_connected; connector->on_failure = irc_on_connector_failure; s->state = IRC_CONNECTING; s->connector = connector; for (size_t i = 0; i < addresses->len; i++) { char *host, *port; irc_split_host_port (addresses->vector[i], &host, &port); if (!connector_add_target (connector, host, port, e)) { irc_destroy_connector (s); return false; } } connector_step (connector); return true; } static bool irc_initiate_connect_socks (struct server *s, const struct str_vector *addresses, struct error **e) { const char *socks_host = get_config_string (s->config, "socks_host"); int64_t socks_port_int = get_config_integer (s->config, "socks_port"); const char *socks_username = get_config_string (s->config, "socks_username"); const char *socks_password = get_config_string (s->config, "socks_password"); if (!socks_host) return false; // FIXME: we only try the first address (still better than nothing) char *irc_host, *irc_port; irc_split_host_port (addresses->vector[0], &irc_host, &irc_port); char *socks_port = xstrdup_printf ("%" PRIi64, socks_port_int); log_server_status (s, s->buffer, "Connecting to #&s via #&s...", format_host_port_pair (irc_host, irc_port), format_host_port_pair (socks_host, socks_port)); // TODO: the SOCKS code needs a rewrite so that we don't block on it either; // perhaps it could act as a special kind of connector struct error *error = NULL; bool result = true; int fd = socks_connect (socks_host, socks_port, irc_host, irc_port, socks_username, socks_password, &error); if (fd != -1) irc_finish_connection (s, fd); else { error_set (e, "%s: %s", "SOCKS connection failed", error->message); error_free (error); result = false; } free (socks_port); return result; } static void irc_initiate_connect (struct server *s) { hard_assert (s->state == IRC_DISCONNECTED); const char *addresses = get_config_string (s->config, "addresses"); if (!addresses || !addresses[strspn (addresses, ",")]) { // No sense in trying to reconnect log_server_error (s, s->buffer, "No addresses specified in configuration"); return; } struct str_vector servers; str_vector_init (&servers); split_str_ignore_empty (addresses, ',', &servers); struct error *e = NULL; if (!irc_initiate_connect_socks (s, &servers, &e) && !e) irc_setup_connector (s, &servers, &e); str_vector_free (&servers); if (e) { log_server_error (s, s->buffer, "#s", e->message); error_free (e); irc_queue_reconnect (s); } } // --- Input prompt ------------------------------------------------------------ static void make_unseen_prefix (struct app_context *ctx, struct str *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, ','); if (iter->highlighted) str_append_c (active_buffers, '!'); str_append_printf (active_buffers, "%zu", i); } } static void make_chanmode_postfix (struct channel *channel, struct str *modes) { if (channel->no_param_modes.len) str_append (modes, channel->no_param_modes.str); struct str_map_iter iter; str_map_iter_init (&iter, &channel->param_modes); char *param; while ((param = str_map_iter_next (&iter))) str_append_c (modes, iter.link->key[0]); } static void make_server_postfix (struct buffer *buffer, struct str *output) { struct server *s = buffer->server; str_append_c (output, ' '); if (!irc_is_connected (s)) str_append (output, "(disconnected)"); else if (s->state != IRC_REGISTERED) str_append (output, "(unregistered)"); else { if (buffer->type == BUFFER_CHANNEL) { struct channel_user *channel_user = irc_channel_get_user (buffer->channel, s->irc_user); if (channel_user) str_append (output, channel_user->prefixes.str); } str_append (output, s->irc_user->nickname); if (s->irc_user_mode.len) str_append_printf (output, "(%s)", s->irc_user_mode.str); } } static void make_prompt (struct app_context *ctx, struct str *output) { struct buffer *buffer = ctx->current_buffer; if (!buffer) return; str_append_c (output, '['); struct str active_buffers; str_init (&active_buffers); make_unseen_prefix (ctx, &active_buffers); if (active_buffers.len) str_append_printf (output, "(%s) ", active_buffers.str); str_free (&active_buffers); str_append_printf (output, "%d:%s", buffer_get_index (ctx, buffer), buffer->name); if (buffer->type == BUFFER_CHANNEL) { struct str modes; str_init (&modes); make_chanmode_postfix (buffer->channel, &modes); if (modes.len) str_append_printf (output, "(+%s)", modes.str); str_free (&modes); } if (buffer != ctx->global_buffer) make_server_postfix (buffer, output); 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); } // --- Helpers ----------------------------------------------------------------- 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) { // 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); if (irc_is_this_us (s, target)) buffer = irc_get_or_make_user_buffer (s, nickname); free (nickname); // With the IRCv3.2 echo-message capability, we can receive messages // as they are delivered to the target; in that case we return NULL // and the caller should check the origin } return buffer; } static bool irc_is_highlight (struct server *s, const char *message) { // This may be called by notices before even successfully registering if (!s->irc_user) return false; // 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); transform_str (copy, s->irc_tolower); char *nick = xstrdup (s->irc_user->nickname); transform_str (nick, s->irc_tolower); // 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 const char * irc_get_privmsg_prefix (struct server *s, struct user *user, const char *target) { if (user && irc_is_channel (s, target)) { struct channel *channel; struct channel_user *channel_user; if ((channel = str_map_find (&s->irc_channels, target)) && (channel_user = irc_channel_get_user (channel, user))) return channel_user->prefixes.str; } return ""; } // --- Mode processor ---------------------------------------------------------- struct mode_processor { char **params; ///< Mode string parameters bool adding; ///< Currently adding modes char mode_char; ///< Currently processed mode char // User data: struct server *s; ///< Server struct channel *channel; ///< The channel being modified }; /// Process a single mode character typedef bool (*mode_processor_apply_fn) (struct mode_processor *); static const char * mode_processor_next_param (struct mode_processor *self) { if (!*self->params) return NULL; return *self->params++; } static void mode_processor_run (struct mode_processor *self, char **params, mode_processor_apply_fn apply_cb) { self->params = params; const char *mode_string; while ((mode_string = mode_processor_next_param (self))) { self->adding = true; while ((self->mode_char = *mode_string++)) { if (self->mode_char == '+') self->adding = true; else if (self->mode_char == '-') self->adding = false; else if (!apply_cb (self)) break; } } } static int mode_char_cmp (const void *a, const void *b) { return *(const char *) a - *(const char *) b; } /// Add/remove the current mode character to/from the given ordered list static void mode_processor_toggle (struct mode_processor *self, struct str *modes) { const char *pos = strchr (modes->str, self->mode_char); if (self->adding == !!pos) return; if (self->adding) { str_append_c (modes, self->mode_char); qsort (modes->str, modes->len, 1, mode_char_cmp); } else str_remove_slice (modes, pos - modes->str, 1); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void mode_processor_do_user (struct mode_processor *self) { const char *nickname; struct user *user; struct channel_user *channel_user; if (!(nickname = mode_processor_next_param (self)) || !(user = str_map_find (&self->s->irc_users, nickname)) || !(channel_user = irc_channel_get_user (self->channel, user))) return; const char *all_prefixes = self->s->irc_chanuser_prefixes; const char *all_modes = self->s->irc_chanuser_modes; char prefix = all_prefixes[strchr (all_modes, self->mode_char) - all_modes]; struct str *prefixes = &channel_user->prefixes; const char *pos = strchr (prefixes->str, prefix); if (self->adding == !!pos) return; if (self->adding) { // Add the new mode prefix while retaining the right order char *old_prefixes = str_steal (prefixes); str_init (prefixes); for (const char *p = all_prefixes; *p; p++) if (*p == prefix || strchr (old_prefixes, *p)) str_append_c (prefixes, *p); free (old_prefixes); } else str_remove_slice (prefixes, pos - prefixes->str, 1); } static void mode_processor_do_param_always (struct mode_processor *self) { const char *param = NULL; if (!(param = mode_processor_next_param (self))) return; char key[2] = { self->mode_char, 0 }; str_map_set (&self->channel->param_modes, key, self->adding ? xstrdup (param) : NULL); } static void mode_processor_do_param_when_set (struct mode_processor *self) { const char *param = NULL; if (self->adding && !(param = mode_processor_next_param (self))) return; char key[2] = { self->mode_char, 0 }; str_map_set (&self->channel->param_modes, key, self->adding ? xstrdup (param) : NULL); } static bool mode_processor_apply_channel (struct mode_processor *self) { if (strchr (self->s->irc_chanuser_modes, self->mode_char)) mode_processor_do_user (self); else if (strchr (self->s->irc_chanmodes_list, self->mode_char)) // Nothing to do here, just skip the next argument if there's any (void) mode_processor_next_param (self); else if (strchr (self->s->irc_chanmodes_param_always, self->mode_char)) mode_processor_do_param_always (self); else if (strchr (self->s->irc_chanmodes_param_when_set, self->mode_char)) mode_processor_do_param_when_set (self); else if (strchr (self->s->irc_chanmodes_param_never, self->mode_char)) mode_processor_toggle (self, &self->channel->no_param_modes); else // It's not safe to continue, results could be undesired return false; return true; } static void irc_handle_mode_channel (struct server *s, struct channel *channel, char **params) { struct mode_processor p = { .s = s, .channel = channel }; mode_processor_run (&p, params, mode_processor_apply_channel); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static bool mode_processor_apply_user (struct mode_processor *self) { mode_processor_toggle (self, &self->s->irc_user_mode); return true; } static void irc_handle_mode_user (struct server *s, char **params) { struct mode_processor p = { .s = s }; mode_processor_run (&p, params, mode_processor_apply_user); } // --- Input handling ---------------------------------------------------------- static void irc_handle_cap (struct server *s, const struct irc_message *msg) { if (msg->params.len < 2) return; struct str_vector v; str_vector_init (&v); const char *args = ""; if (msg->params.len > 2) split_str_ignore_empty ((args = msg->params.vector[2]), ' ', &v); const char *subcommand = msg->params.vector[1]; if (!strcasecmp_ascii (subcommand, "ACK")) { log_server_status (s, s->buffer, "#s: #S", "Capabilities acknowledged", args); for (size_t i = 0; i < v.len; i++) { const char *cap = v.vector[i]; bool active = true; if (*cap == '-') { active = false; cap++; } if (!strcasecmp_ascii (cap, "echo-message")) s->cap_echo_message = active; } irc_send (s, "CAP END"); } else if (!strcasecmp_ascii (subcommand, "NAK")) { log_server_error (s, s->buffer, "#s: #S", "Capabilities not acknowledged", args); irc_send (s, "CAP END"); } else if (!strcasecmp_ascii (subcommand, "LS")) { log_server_status (s, s->buffer, "#s: #S", "Capabilities supported", args); struct str_vector chosen; str_vector_init (&chosen); // Filter server capabilities for ones we can make use of for (size_t i = 0; i < v.len; i++) { const char *cap = v.vector[i]; if (!strcasecmp_ascii (cap, "multi-prefix") || !strcasecmp_ascii (cap, "invite-notify") || !strcasecmp_ascii (cap, "echo-message")) str_vector_add (&chosen, cap); } char *chosen_str = join_str_vector (&chosen, ' '); str_vector_free (&chosen); irc_send (s, "CAP REQ :%s", chosen_str); log_server_status (s, s->buffer, "#s: #S", "Capabilities requested", chosen_str); free (chosen_str); } str_vector_free (&v); } static void irc_handle_invite (struct server *s, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 2) return; const char *target = msg->params.vector[0]; const char *channel_name = msg->params.vector[1]; struct buffer *buffer; if (!(buffer = str_map_find (&s->irc_buffer_map, channel_name))) buffer = s->buffer; // IRCv3.2 invite-notify extension allows the target to be someone else if (irc_is_this_us (s, target)) log_server_status (s, buffer, "#n has invited you to #S", msg->prefix, channel_name); else log_server_status (s, buffer, "#n has invited #n to #S", msg->prefix, target, channel_name); } 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_printf ("%s.%s", s->name, channel_name); buffer->server = s; buffer->channel = channel = irc_make_channel (s, xstrdup (channel_name)); str_map_set (&s->irc_buffer_map, channel->name, buffer); buffer_add (s->ctx, buffer); buffer_activate (s->ctx, buffer); // Request the channel mode as we don't get it automatically irc_send (s, "MODE %s", channel_name); } // This is weird, ignoring if (!channel) return; // Add the user to the channel char *nickname = irc_cut_nickname (msg->prefix); irc_channel_link_user (channel, irc_get_or_make_user (s, nickname), ""); free (nickname); // Finally log the message if (buffer) { log_server (s, buffer, 0, "#a-->#r #N #a#s#r #S", ATTR_JOIN, msg->prefix, ATTR_JOIN, "has joined", 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 = NULL; 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) { if (irc_is_this_us (s, target)) irc_left_channel (channel); else irc_remove_user_from_channel (user, channel); } if (buffer) { struct formatter f; formatter_init (&f, s->ctx, s); formatter_add (&f, "#a<--#r #N #a#s#r #n", ATTR_PART, msg->prefix, ATTR_PART, "has kicked", target); if (message) formatter_add (&f, " (#m)", message); log_formatter (s->ctx, buffer, 0, &f); } } static void irc_handle_mode (struct server *s, const struct irc_message *msg) { if (!msg->prefix || msg->params.len < 1) return; const char *context = msg->params.vector[0]; // Join the modes back to a single string struct str_vector copy; str_vector_init (©); str_vector_add_vector (©, msg->params.vector + 1); char *modes = join_str_vector (©, ' '); str_vector_free (©); if (irc_is_channel (s, context)) { struct channel *channel = str_map_find (&s->irc_channels, context); struct buffer *buffer = str_map_find (&s->irc_buffer_map, context); hard_assert ((channel && buffer) || (channel && !buffer) || (!channel && !buffer)); if (channel) irc_handle_mode_channel (s, channel, msg->params.vector + 1); if (buffer) { log_server_status (s, buffer, "Mode #S [#S] by #n", context, modes, msg->prefix); } } else if (irc_is_this_us (s, context)) { irc_handle_mode_user (s, msg->params.vector + 1); log_server_status (s, s->buffer, "User mode [#S] by #n", modes, msg->prefix); } free (modes); // Our own modes might have changed refresh_prompt (s->ctx); } 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; bool lexicographically_different = !!irc_server_strcmp (s, user->nickname, new_nickname); // What the fuck, someone renamed themselves to ourselves // TODO: probably log a message and force a reconnect if (lexicographically_different && !irc_server_strcmp (s, new_nickname, s->irc_user->nickname)) return; // Log a message in any PM buffer (we may even have one for ourselves) struct buffer *pm_buffer = str_map_find (&s->irc_buffer_map, user->nickname); if (pm_buffer) { if (irc_is_this_us (s, msg->prefix)) log_nick_self (s, pm_buffer, new_nickname); else log_nick (s, pm_buffer, msg->prefix, new_nickname); } // The new nickname may collide with a user referenced by a PM buffer, // or in case of data inconsistency with the server, channels. // In the latter case we need the colliding user to leave all of them. struct user *user_collision = NULL; if (lexicographically_different && (user_collision = str_map_find (&s->irc_users, new_nickname))) LIST_FOR_EACH (struct user_channel, iter, user_collision->channels) irc_remove_user_from_channel (user_collision, iter->channel); struct buffer *buffer_collision = NULL; if (lexicographically_different && (buffer_collision = str_map_find (&s->irc_buffer_map, new_nickname))) { hard_assert (buffer_collision->type == BUFFER_PM); hard_assert (buffer_collision->user == user_collision); user_unref (buffer_collision->user); buffer_collision->user = user_ref (user); } if (pm_buffer && buffer_collision) { // There's not much else we can do other than somehow try to merge // one buffer into the other. In our case, the original buffer wins. buffer_merge (s->ctx, buffer_collision, pm_buffer); if (s->ctx->current_buffer == pm_buffer) buffer_activate (s->ctx, buffer_collision); buffer_remove (s->ctx, pm_buffer); pm_buffer = buffer_collision; } // The colliding user should be completely gone by now if (lexicographically_different) hard_assert (!str_map_find (&s->irc_users, new_nickname)); // Now we can rename the PM buffer to reflect the new nickname if (pm_buffer) { str_map_set (&s->irc_buffer_map, user->nickname, NULL); str_map_set (&s->irc_buffer_map, new_nickname, pm_buffer); char *x = xstrdup_printf ("%s.%s", s->name, new_nickname); buffer_rename (s->ctx, pm_buffer, x); free (x); } if (irc_is_this_us (s, msg->prefix)) { log_nick_self (s, s->buffer, new_nickname); // 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) log_nick_self (s, buffer, 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); log_nick (s, buffer, msg->prefix, new_nickname); } } // Finally rename the user as it should be safe now str_map_set (&s->irc_users, user->nickname, NULL); str_map_set (&s->irc_users, new_nickname, user); 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) { const char *target = msg->params.vector[0]; if (irc_is_this_us (s, msg->prefix)) log_ctcp_reply (s, target, xstrdup_printf ("%s %s", chunk->tag.str, chunk->text.str)); else log_server_status (s, s->buffer, "CTCP reply from #n: #S #S", msg->prefix, chunk->tag.str, chunk->text.str); } 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) { if (irc_is_this_us (s, msg->prefix)) log_outcoming_orphan_notice (s, target, text->str); return; } char *nick = irc_cut_nickname (msg->prefix); // IRCv3.2 echo-message could otherwise cause us to highlight ourselves if (!irc_is_this_us (s, msg->prefix) && irc_is_highlight (s, text->str)) log_server (s, buffer, BUFFER_LINE_STATUS | BUFFER_LINE_HIGHLIGHT, "#a#s(#S)#r: #m", ATTR_HIGHLIGHT, "Notice", nick, text->str); else log_outcoming_notice (s, buffer, msg->prefix, text->str); free (nick); } 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 = NULL; 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) { if (irc_is_this_us (s, msg->prefix)) irc_left_channel (channel); else irc_remove_user_from_channel (user, channel); } if (buffer) { struct formatter f; formatter_init (&f, s->ctx, s); formatter_add (&f, "#a<--#r #N #a#s#r #S", ATTR_PART, msg->prefix, ATTR_PART, "has left", channel_name); if (message) formatter_add (&f, " (#m)", message); log_formatter (s->ctx, buffer, 0, &f); } } 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); if (!s->cap_echo_message) log_ctcp_reply (s, recipient, str_steal (&m)); else str_free (&m); } static void irc_handle_ctcp_request (struct server *s, const struct irc_message *msg, struct ctcp_chunk *chunk) { const char *target = msg->params.vector[0]; if (irc_is_this_us (s, msg->prefix)) log_ctcp_query (s, target, chunk->tag.str); log_server_status (s, s->buffer, "CTCP requested by #n: #S", msg->prefix, chunk->tag.str); char *recipient = irc_is_channel (s, target) ? irc_cut_nickname (msg->prefix) : xstrdup (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 (recipient); } 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) { if (irc_is_this_us (s, msg->prefix)) log_outcoming_orphan_privmsg (s, target, text->str); return; } char *nickname = irc_cut_nickname (msg->prefix); const char *prefixes = irc_get_privmsg_prefix (s, str_map_find (&s->irc_users, nickname), target); // IRCv3.2 echo-message could otherwise cause us to highlight ourselves if (irc_is_this_us (s, msg->prefix) || !irc_is_highlight (s, text->str)) { if (is_action) log_outcoming_action (s, buffer, nickname, text->str); else log_outcoming_privmsg (s, buffer, prefixes, nickname, text->str); } else if (is_action) log_server (s, buffer, BUFFER_LINE_HIGHLIGHT, " #a*#r #n #m", ATTR_HIGHLIGHT, msg->prefix, text->str); else log_server (s, buffer, BUFFER_LINE_HIGHLIGHT, "#a<#S#S>#r #m", ATTR_HIGHLIGHT, prefixes, nickname, text->str); free (nickname); } 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 log_quit (struct server *s, struct buffer *buffer, const char *prefix, const char *reason) { struct formatter f; formatter_init (&f, s->ctx, s); formatter_add (&f, "#a<--#r #N #a#s#r", ATTR_PART, prefix, ATTR_PART, "has quit"); if (reason) formatter_add (&f, " (#m)", reason); log_formatter (s->ctx, buffer, 0, &f); } static void irc_handle_quit (struct server *s, const struct irc_message *msg) { if (!msg->prefix) return; // What the fuck, the server never sends this back 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 = NULL; 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) { log_quit (s, buffer, msg->prefix, message); // TODO: set some kind of a flag in the buffer and when the user // reappears 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) { if ((buffer = str_map_find (&s->irc_buffer_map, iter->channel->name))) log_quit (s, buffer, msg->prefix, 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) { log_server (s, buffer, BUFFER_LINE_STATUS, "#n #s \"#m\"", msg->prefix, "has changed the topic to", topic); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static struct irc_handler { const char *name; void (*handler) (struct server *s, const struct irc_message *msg); } g_irc_handlers[] = { // This list needs to stay sorted { "CAP", irc_handle_cap }, { "INVITE", irc_handle_invite }, { "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_on_registered (struct server *s, const char *nickname) { s->irc_user = irc_get_or_make_user (s, nickname); str_reset (&s->irc_user_mode); s->irc_user_host = NULL; s->state = IRC_REGISTERED; refresh_prompt (s->ctx); // XXX: 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->config, "autojoin"); if (autojoin) irc_send (s, "JOIN :%s", autojoin); // TODO: rejoin all current channels (mark those we've left manually?) } static void irc_handle_rpl_userhost (struct server *s, const struct irc_message *msg) { if (msg->params.len < 2) return; const char *response = msg->params.vector[1]; struct str_vector v; str_vector_init (&v); split_str_ignore_empty (response, ' ', &v); for (size_t i = 0; i < v.len; i++) { char *nick = v.vector[i]; char *equals = strchr (nick, '='); if (!equals || equals == nick) continue; // User is an IRC operator if (equals[-1] == '*') equals[-1] = '\0'; else equals[ 0] = '\0'; // TODO: make use of this (away status polling?) char away_status = equals[1]; if (!strchr ("+-", away_status)) continue; char *userhost = equals + 2; if (irc_is_this_us (s, nick)) { free (s->irc_user_host); s->irc_user_host = xstrdup (userhost); } } str_vector_free (&v); } static void irc_handle_rpl_umodeis (struct server *s, const struct irc_message *msg) { if (msg->params.len < 2) return; str_reset (&s->irc_user_mode); irc_handle_mode_user (s, msg->params.vector + 1); // XXX: do we want to log a message? refresh_prompt (s->ctx); } static void irc_handle_rpl_namreply (struct server *s, const struct irc_message *msg) { if (msg->params.len < 4) return; const char *channel_name = msg->params.vector[2]; const char *nicks = msg->params.vector[3]; // Just push the nicknames to a string vector to process later struct channel *channel = str_map_find (&s->irc_channels, channel_name); if (channel) split_str_ignore_empty (nicks, ' ', &channel->names_buf); } static void irc_sync_channel_user (struct server *s, struct channel *channel, const char *nickname, const char *prefixes) { struct user *user = irc_get_or_make_user (s, nickname); struct channel_user *channel_user = irc_channel_get_user (channel, user); if (!channel_user) { irc_channel_link_user (channel, user, prefixes); return; } user_unref (user); // If our idea of the user's modes disagrees with what the server's // sent us (the most powerful modes differ), use the latter one if (channel_user->prefixes.str[0] != prefixes[0]) { str_reset (&channel_user->prefixes); str_append (&channel_user->prefixes, prefixes); } } static void irc_process_names (struct server *s, struct channel *channel) { struct str_map present; str_map_init (&present); present.key_xfrm = s->irc_strxfrm; struct str_vector *updates = &channel->names_buf; for (size_t i = 0; i < updates->len; i++) { const char *item = updates->vector[i]; size_t n_prefixes = strspn (item, s->irc_chanuser_prefixes); const char *nickname = item + n_prefixes; // Store the nickname in a hashset str_map_set (&present, nickname, (void *) 1); char *prefixes = xstrndup (item, n_prefixes); irc_sync_channel_user (s, channel, nickname, prefixes); free (prefixes); } // Get rid of channel users missing from "updates" LIST_FOR_EACH (struct channel_user, iter, channel->users) if (!str_map_find (&present, iter->user->nickname)) irc_channel_unlink_user (channel, iter); str_map_free (&present); str_vector_reset (&channel->names_buf); struct str_vector v; str_vector_init (&v); LIST_FOR_EACH (struct channel_user, iter, channel->users) str_vector_add_owned (&v, xstrdup_printf ("%s%s", iter->prefixes.str, iter->user->nickname)); char *all_users = join_str_vector (&v, ' '); str_vector_free (&v); struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel->name); if (buffer) { log_server_status (s, buffer, "Users on #S: #S", channel->name, all_users); } free (all_users); } static void irc_handle_rpl_endofnames (struct server *s, const struct irc_message *msg) { if (msg->params.len < 2) return; const char *channel_name = msg->params.vector[1]; struct channel *channel = str_map_find (&s->irc_channels, channel_name); if (!strcmp (channel_name, "*")) { struct str_map_iter iter; str_map_iter_init (&iter, &s->irc_channels); struct channel *channel; while ((channel = str_map_iter_next (&iter))) irc_process_names (s, channel); } else if (channel) irc_process_names (s, channel); } static void irc_handle_rpl_topic (struct server *s, const struct irc_message *msg) { if (msg->params.len < 3) return; const char *channel_name = msg->params.vector[1]; const char *topic = msg->params.vector[2]; 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)); if (channel) { free (channel->topic); channel->topic = xstrdup (topic); } if (buffer) log_server_status (s, buffer, "The topic is: #m", topic); } static void irc_handle_rpl_channelmodeis (struct server *s, const struct irc_message *msg) { if (msg->params.len < 2) return; const char *channel_name = msg->params.vector[1]; 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)); if (channel) { str_reset (&channel->no_param_modes); str_map_clear (&channel->param_modes); irc_handle_mode_channel (s, channel, msg->params.vector + 1); } // XXX: do we want to log a message? refresh_prompt (s->ctx); } static char * make_time_string (time_t time) { char buf[32]; struct tm tm; strftime (buf, sizeof buf, "%a %b %d %Y %T", localtime_r (&time, &tm)); return xstrdup (buf); } static void irc_handle_rpl_creationtime (struct server *s, const struct irc_message *msg) { if (msg->params.len < 3) return; const char *channel_name = msg->params.vector[1]; const char *creation_time = msg->params.vector[2]; unsigned long created; if (!xstrtoul (&created, creation_time, 10)) 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)); if (buffer) { log_server_status (s, buffer, "Channel created on #&s", make_time_string (created)); } } static void irc_handle_rpl_topicwhotime (struct server *s, const struct irc_message *msg) { if (msg->params.len < 4) return; const char *channel_name = msg->params.vector[1]; const char *who = msg->params.vector[2]; const char *change_time = msg->params.vector[3]; unsigned long changed; if (!xstrtoul (&changed, change_time, 10)) 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)); if (buffer) { log_server_status (s, buffer, "Topic set by #N on #&s", who, make_time_string (changed)); } } static void irc_handle_rpl_inviting (struct server *s, const struct irc_message *msg) { if (msg->params.len < 3) return; const char *channel_name = msg->params.vector[1]; const char *nickname = msg->params.vector[2]; struct buffer *buffer; if (!(buffer = str_map_find (&s->irc_buffer_map, channel_name))) buffer = s->buffer; log_server_status (s, buffer, "You have invited #n to #S", nickname, channel_name); } static void irc_handle_err_nicknameinuse (struct server *s, const struct irc_message *msg) { if (msg->params.len < 2) return; log_server_error (s, s->buffer, "Nickname is already in use: #S", msg->params.vector[1]); // Only do this while we haven't successfully registered yet if (s->state != IRC_CONNECTED) return; char *nickname = irc_fetch_next_nickname (s); if (nickname) { log_server_status (s, s->buffer, "Retrying with #s...", nickname); irc_send (s, "NICK :%s", nickname); free (nickname); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void irc_handle_isupport_prefix (struct server *s, char *value) { char *modes = value; char *prefixes = strchr (value, ')'); size_t n_prefixes = prefixes - modes; if (*modes++ != '(' || !prefixes++ || strlen (value) != 2 * n_prefixes--) return; free (s->irc_chanuser_modes); free (s->irc_chanuser_prefixes); s->irc_chanuser_modes = xstrndup (modes, n_prefixes); s->irc_chanuser_prefixes = xstrndup (prefixes, n_prefixes); } static void irc_handle_isupport_casemapping (struct server *s, char *value) { if (!strcmp (value, "ascii")) irc_set_casemapping (s, tolower_ascii, tolower_ascii_strxfrm); else if (!strcmp (value, "rfc1459")) irc_set_casemapping (s, irc_tolower, irc_strxfrm); else if (!strcmp (value, "rfc1459-strict")) irc_set_casemapping (s, irc_tolower_strict, irc_strxfrm_strict); } static void irc_handle_isupport_chantypes (struct server *s, char *value) { free (s->irc_chantypes); s->irc_chantypes = xstrdup (value); } static void irc_handle_isupport_idchan (struct server *s, char *value) { struct str prefixes; str_init (&prefixes); struct str_vector v; str_vector_init (&v); split_str_ignore_empty (value, ',', &v); for (size_t i = 0; i < v.len; i++) { // Not using or validating the numeric part const char *pair = v.vector[i]; const char *colon = strchr (pair, ':'); if (colon) str_append_data (&prefixes, pair, colon - pair); } str_vector_free (&v); free (s->irc_idchan_prefixes); s->irc_idchan_prefixes = str_steal (&prefixes); } static void irc_handle_isupport_statusmsg (struct server *s, char *value) { free (s->irc_statusmsg); s->irc_statusmsg = xstrdup (value); } static void irc_handle_isupport_chanmodes (struct server *s, char *value) { struct str_vector v; str_vector_init (&v); split_str_ignore_empty (value, ',', &v); if (v.len >= 4) { free (s->irc_chanmodes_list); s->irc_chanmodes_list = xstrdup (v.vector[0]); free (s->irc_chanmodes_param_always); s->irc_chanmodes_param_always = xstrdup (v.vector[1]); free (s->irc_chanmodes_param_when_set); s->irc_chanmodes_param_when_set = xstrdup (v.vector[2]); free (s->irc_chanmodes_param_never); s->irc_chanmodes_param_never = xstrdup (v.vector[3]); } str_vector_free (&v); } static void irc_handle_isupport_modes (struct server *s, char *value) { unsigned long modes; if (!*value) s->irc_max_modes = UINT_MAX; else if (xstrtoul (&modes, value, 10) && modes && modes <= UINT_MAX) s->irc_max_modes = modes; } static void unescape_isupport_value (const char *value, struct str *output) { const char *alphabet = "0123456789abcdef", *a, *b; for (const char *p = value; *p; p++) { if (p[0] == '\\' && p[1] == 'x' && p[2] && (a = strchr (alphabet, tolower_ascii (p[2]))) && p[3] && (b = strchr (alphabet, tolower_ascii (p[3])))) { str_append_c (output, (a - alphabet) << 4 | (b - alphabet)); p += 3; } else str_append_c (output, *p); } } static void dispatch_isupport (struct server *s, const char *name, char *value) { #define MATCH(from, to) if (!strcmp (name, (from))) { (to) (s, value); return; } // TODO: also make use of TARGMAX to split client commands as necessary MATCH ("PREFIX", irc_handle_isupport_prefix); MATCH ("CASEMAPPING", irc_handle_isupport_casemapping); MATCH ("CHANTYPES", irc_handle_isupport_chantypes); MATCH ("IDCHAN", irc_handle_isupport_idchan); MATCH ("STATUSMSG", irc_handle_isupport_statusmsg); MATCH ("CHANMODES", irc_handle_isupport_chanmodes); MATCH ("MODES", irc_handle_isupport_modes); #undef MATCH } static void irc_handle_rpl_isupport (struct server *s, const struct irc_message *msg) { if (msg->params.len < 2) return; for (size_t i = 1; i < msg->params.len - 1; i++) { // TODO: if the parameter starts with "-", it resets to default char *param = msg->params.vector[i]; char *value = param + strcspn (param, "="); if (*value) *value++ = '\0'; struct str value_unescaped; str_init (&value_unescaped); unescape_isupport_value (value, &value_unescaped); dispatch_isupport (s, param, value_unescaped.str); str_free (&value_unescaped); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void irc_process_numeric (struct server *s, const struct irc_message *msg, unsigned long numeric) { // Numerics typically have human-readable information // 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); struct buffer *buffer = s->buffer; switch (numeric) { case IRC_RPL_WELCOME: irc_on_registered (s, msg->params.vector[0]); // 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: irc_handle_rpl_isupport (s, msg); break; case IRC_RPL_USERHOST: irc_handle_rpl_userhost (s, msg); break; case IRC_RPL_UMODEIS: irc_handle_rpl_umodeis (s, msg); buffer = NULL; break; case IRC_RPL_NAMREPLY: irc_handle_rpl_namreply (s, msg); buffer = NULL; break; case IRC_RPL_ENDOFNAMES: irc_handle_rpl_endofnames (s, msg); buffer = NULL; break; case IRC_RPL_TOPIC: irc_handle_rpl_topic (s, msg); buffer = NULL; break; case IRC_RPL_CHANNELMODEIS: irc_handle_rpl_channelmodeis (s, msg); buffer = NULL; break; case IRC_RPL_CREATIONTIME: irc_handle_rpl_creationtime (s, msg); buffer = NULL; break; case IRC_RPL_TOPICWHOTIME: irc_handle_rpl_topicwhotime (s, msg); buffer = NULL; break; case IRC_RPL_INVITING: irc_handle_rpl_inviting (s, msg); buffer = NULL; break; case IRC_ERR_NICKNAMEINUSE: irc_handle_err_nicknameinuse (s, msg); buffer = NULL; break; case IRC_RPL_LIST: case IRC_RPL_WHOREPLY: case IRC_RPL_ENDOFWHO: case IRC_ERR_UNKNOWNCOMMAND: case IRC_ERR_NEEDMOREPARAMS: // Just preventing these commands from getting printed in a more // specific buffer as that would be unwanted break; default: // If the second parameter is something we have a buffer for // (a channel, a PM buffer), log it in that buffer. This is very basic. // TODO: whitelist/blacklist a lot more replies in here. // TODO: we should either strip the first parameter from the resulting // buffer line, or at least put it in brackets if (msg->params.len > 1) { struct buffer *x; if ((x = str_map_find (&s->irc_buffer_map, msg->params.vector[1]))) buffer = x; } } if (buffer) { // Join the parameter vector back and send it to the server buffer log_server (s, buffer, BUFFER_LINE_STATUS, "#&m", join_str_vector (©, ' ')); } str_vector_free (©); } static void irc_process_message (const struct irc_message *msg, const char *raw, void *user_data) { struct server *s = user_data; log_server_debug (s, "#a>> \"#S\"#r", ATTR_JOIN, raw); 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 && 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); // We might also want to preserve attributes across splits but // that would make this code a lot more complicated struct str_vector lines; str_vector_init (&lines); struct error *e = NULL; if (!irc_autosplit_message (s, a.message, fixed_part, &lines, &e)) { log_server_error (s, 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); if (!s->cap_echo_message) a.logger (s, &a, buffer, lines.vector[i]); } end: str_vector_free (&lines); } static void log_autosplit_action (struct server *s, struct send_autosplit_args *a, struct buffer *buffer, const char *line) { (void) a; if (buffer && soft_assert (s->irc_user)) log_outcoming_action (s, buffer, s->irc_user->nickname, 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_autosplit_action, \ "\x01" "ACTION ", "\x01" }) static void log_autosplit_privmsg (struct server *s, struct send_autosplit_args *a, struct buffer *buffer, const char *line) { const char *prefixes = irc_get_privmsg_prefix (s, s->irc_user, a->target); if (buffer && soft_assert (s->irc_user)) log_outcoming_privmsg (s, buffer, prefixes, s->irc_user->nickname, line); else log_outcoming_orphan_privmsg (s, a->target, line); } #define SEND_AUTOSPLIT_PRIVMSG(s, target, message) \ send_autosplit_message ((s), (struct send_autosplit_args) \ { "PRIVMSG", (target), (message), log_autosplit_privmsg, "", "" }) static void log_autosplit_notice (struct server *s, struct send_autosplit_args *a, struct buffer *buffer, const char *line) { if (buffer && soft_assert (s->irc_user)) log_outcoming_notice (s, buffer, s->irc_user->nickname, line); else log_outcoming_orphan_notice (s, a->target, line); } #define SEND_AUTOSPLIT_NOTICE(s, target, message) \ send_autosplit_message ((s), (struct send_autosplit_args) \ { "NOTICE", (target), (message), log_autosplit_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) { // Empty objects will show as such if (item->type == CONFIG_ITEM_OBJECT && item->value.object.len) { config_dump_children (item, data); return; } 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); struct str value; str_init (&value); config_item_write (item, false, &value); // 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, " = "); str_append_str (&line, &value); } if (!schema) str_append (&line, " (unrecognized)"); else 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 char *key = str_cut_until (output->vector[i], " "); if (fnmatch (mask, key, 0)) str_vector_remove (output, i--); free (key); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void save_configuration (struct app_context *ctx) { 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) { log_global_error (ctx, "#s: #s", "Saving configuration failed", e->message); error_free (e); } else log_global_status (ctx, "Configuration written to `#s'", filename); free (filename); } // --- Server management ------------------------------------------------------- static bool validate_server_name (const char *name) { for (const unsigned char *p = (const unsigned char *) name; *p; p++) if (*p < 32 || *p == '.') return false; return true; } static bool check_server_name_for_addition (struct app_context *ctx, const char *name) { if (!strcasecmp_ascii (name, ctx->global_buffer->name)) log_global_error (ctx, "Cannot create server `#s': #s", name, "name collides with the global buffer"); else if (str_map_find (&ctx->servers, name)) log_global_error (ctx, "Cannot create server `#s': #s", name, "server already exists"); else if (!validate_server_name (name)) log_global_error (ctx, "Cannot create server `#s': #s", name, "invalid server name"); else return true; return false; } static struct server * server_add (struct app_context *ctx, const char *name, struct config_item_ *subtree) { hard_assert (!str_map_find (&ctx->servers, name)); struct server *s = xmalloc (sizeof *s); server_init (s, &ctx->poller); s->ctx = ctx; s->name = xstrdup (name); str_map_set (&ctx->servers, s->name, s); s->config = subtree; // Add a buffer and activate it struct buffer *buffer = s->buffer = buffer_new (); buffer->type = BUFFER_SERVER; buffer->name = xstrdup (s->name); buffer->server = s; buffer_add (ctx, buffer); buffer_activate (ctx, buffer); config_schema_apply_to_object (g_config_server, subtree, s); config_schema_call_changed (subtree); if (get_config_boolean (s->config, "autoconnect")) // Connect to the server ASAP poller_timer_set (&s->reconnect_tmr, 0); return s; } static void server_add_new (struct app_context *ctx, const char *name) { // Note that there may already be something in the configuration under // that key that we've ignored earlier, and there may also be // a case-insensitive conflict. Those things may only happen as a result // of manual edits to the configuration, though, and they're not really // going to break anything. They only cause surprises when loading. struct str_map *servers = get_servers_config (ctx); struct config_item_ *subtree = config_item_object (); str_map_set (servers, name, subtree); struct server *s = server_add (ctx, name, subtree); struct error *e = NULL; if (!irc_autofill_user_info (s, &e)) { log_server_error (s, s->buffer, "#s: #s", "Failed to fill in user details", e->message); error_free (e); } } // --- User input handling ----------------------------------------------------- // HANDLER_NEEDS_REG is primarily for message sending commands, // as they may want to log buffer lines and use our current nickname enum handler_flags { HANDLER_SERVER = (1 << 0), ///< Server context required HANDLER_NEEDS_REG = (1 << 1), ///< Server registration required HANDLER_CHANNEL_FIRST = (1 << 2), ///< Channel required, first argument HANDLER_CHANNEL_LAST = (1 << 3) ///< Channel required, last argument }; struct handler_args { struct app_context *ctx; ///< Application context struct buffer *buffer; ///< Current buffer struct server *s; ///< Related server const char *channel_name; ///< Related channel name char *arguments; ///< Command arguments }; /// 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; } /// Validates a word to be cut from a string typedef bool (*word_validator_fn) (void *, char *); static char * maybe_cut_word (char **s, word_validator_fn validator, void *user_data) { char *start = *s; size_t word_len = strcspn (*s, WORD_BREAKING_CHARS); char *word = xstrndup (start, word_len); bool ok = validator (user_data, word); free (word); if (!ok) return NULL; char *end = start + word_len; *s = end + strspn (end, WORD_BREAKING_CHARS); *end = '\0'; return start; } static char * maybe_cut_word_from_end (char **s, word_validator_fn validator, void *user_data) { // Find the start and end of the last word char *start = *s, *end = start + strlen (start); while (end > start && strchr (WORD_BREAKING_CHARS, end [-1])) end--; char *word = end; while (word > start && !strchr (WORD_BREAKING_CHARS, word[-1])) word--; // There's just one word at maximum, starting at the beginning if (word == start) return maybe_cut_word (s, validator, user_data); char *tmp = xstrndup (word, word - start); bool ok = validator (user_data, tmp); free (tmp); if (!ok) return NULL; // It doesn't start at the beginning, cut it off and return it word[-1] = *end = '\0'; return word; } static bool validate_channel_name (void *user_data, char *word) { return irc_is_channel (user_data, word); } static char * try_get_channel (struct handler_args *a, char *(*cutter) (char **, word_validator_fn, void *)) { char *channel_name = cutter (&a->arguments, validate_channel_name, a->s); if (channel_name) return channel_name; if (a->buffer->type == BUFFER_CHANNEL) return a->buffer->channel->name; return NULL; } 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)) log_global_error (ctx, "#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: partial matches return buffer; } static void show_buffers_list (struct app_context *ctx) { log_global_indent (ctx, ""); log_global_indent (ctx, "Buffers list:"); int i = 1; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) log_global_indent (ctx, " [#d] #s", i++, iter->name); } static void handle_buffer_close (struct app_context *ctx, struct handler_args *a) { struct buffer *buffer = NULL; const char *which = NULL; if (!*a->arguments) buffer = a->buffer; else buffer = try_decode_buffer (ctx, (which = cut_word (&a->arguments))); if (!buffer) log_global_error (ctx, "#s: #s", "No such buffer", which); else if (buffer == ctx->global_buffer) log_global_error (ctx, "Can't close the global buffer"); else if (buffer->type == BUFFER_SERVER) log_global_error (ctx, "Can't close a server buffer"); else { // The user would be unable to recreate the buffer otherwise if (buffer->type == BUFFER_CHANNEL) irc_send (buffer->server, "PART %s", buffer->channel->name); if (buffer == ctx->current_buffer) buffer_activate (ctx, ctx->last_buffer ? ctx->last_buffer : buffer_next (ctx, 1)); buffer_remove (ctx, buffer); } } static bool handle_buffer_move (struct app_context *ctx, struct handler_args *a) { unsigned long request; if (!xstrtoul (&request, a->arguments, 10)) return false; unsigned long total = 0; LIST_FOR_EACH (struct buffer, iter, ctx->buffers) total++; if (request == 0 || request > total) { log_global_error (ctx, "#s: #s", "Can't move buffer", "requested position is out of range"); return true; } struct buffer *buffer = a->buffer; LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer); struct buffer *following = ctx->buffers; while (--request && following) following = following->next; LIST_INSERT_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer, following); refresh_prompt (ctx); return true; } static bool handle_command_buffer (struct handler_args *a) { struct app_context *ctx = a->ctx; char *action = cut_word (&a->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 bool result = true; if (!strcasecmp_ascii (action, "list")) show_buffers_list (ctx); else if (!strcasecmp_ascii (action, "clear")) { buffer_clear (a->buffer); // XXX: clear screen? buffer_print_backlog (ctx, a->buffer); } else if (!strcasecmp_ascii (action, "move")) result = handle_buffer_move (ctx, a); else if (!strcasecmp_ascii (action, "close")) handle_buffer_close (ctx, a); else result = false; return result; } 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 (!item->schema) error_set (&e, "option not recognized"); else 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) { log_global_error (ctx, "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); log_global_status (ctx, "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) { log_global_error (ctx, "Invalid value: #s", e->message); error_free (e); return true; } if ((add | remove) && !config_item_type_is_string (new_->type)) { log_global_error (ctx, "+= / -= operators need a string argument"); config_item_destroy (new_); return true; } for (size_t i = 0; i < all->len; i++) { char *key = str_cut_until (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 handler_args *a) { struct app_context *ctx = a->ctx; char *option = "*"; if (*a->arguments) option = cut_word (&a->arguments); struct str_vector all; str_vector_init (&all); dump_matching_options (ctx, option, &all); bool result = true; if (!all.len) log_global_error (ctx, "No matches: #s", option); else if (!*a->arguments) { log_global_indent (ctx, ""); for (size_t i = 0; i < all.len; i++) log_global_indent (ctx, "#s", all.vector[i]); } else result = handle_command_set_assign (ctx, &all, a->arguments); str_vector_free (&all); return result; } static bool handle_command_save (struct handler_args *a) { if (*a->arguments) return false; save_configuration (a->ctx); return true; } static bool show_aliases_list (struct app_context *ctx) { log_global_indent (ctx, ""); log_global_indent (ctx, "Aliases:"); struct str_map *aliases = get_aliases_config (ctx); if (!aliases->len) { log_global_indent (ctx, " (none)"); return true; } struct str_map_iter iter; str_map_iter_init (&iter, aliases); struct config_item_ *alias; while ((alias = str_map_iter_next (&iter))) { struct str definition; str_init (&definition); if (config_item_type_is_string (alias->type)) config_item_write_string (&definition, &alias->value.string); else str_append (&definition, "alias definition is not a string"); log_global_indent (ctx, " /#s: #s", iter.link->key, definition.str); str_free (&definition); } return true; } static bool handle_command_alias (struct handler_args *a) { if (!*a->arguments) return show_aliases_list (a->ctx); // TODO: validate the name; maybe also while loading configuration char *name = cut_word (&a->arguments); if (!*a->arguments) return false; struct config_item_ *alias = config_item_string_from_cstr (a->arguments); struct str definition; str_init (&definition); config_item_write_string (&definition, &alias->value.string); str_map_set (get_aliases_config (a->ctx), name, alias); log_global_status (a->ctx, "Created alias /#s: #s", name, definition.str); str_free (&definition); return true; } static bool handle_command_unalias (struct handler_args *a) { if (!*a->arguments) return false; struct str_map *aliases = get_aliases_config (a->ctx); while (*a->arguments) { char *name = cut_word (&a->arguments); if (!str_map_find (aliases, name)) log_global_error (a->ctx, "No such alias: #s", name); else { str_map_set (aliases, name, NULL); log_global_status (a->ctx, "Alias removed: #s", name); } } return true; } static bool handle_command_msg (struct handler_args *a) { if (!*a->arguments) return false; char *target = cut_word (&a->arguments); if (!*a->arguments) log_server_error (a->s, a->s->buffer, "No text to send"); else SEND_AUTOSPLIT_PRIVMSG (a->s, target, a->arguments); return true; } static bool handle_command_query (struct handler_args *a) { if (!*a->arguments) return false; char *target = cut_word (&a->arguments); if (irc_is_channel (a->s, target)) log_server_error (a->s, a->s->buffer, "Cannot query a channel"); else if (!*a->arguments) log_server_error (a->s, a->s->buffer, "No text to send"); else { buffer_activate (a->ctx, irc_get_or_make_user_buffer (a->s, target)); SEND_AUTOSPLIT_PRIVMSG (a->s, target, a->arguments); } return true; } static bool handle_command_notice (struct handler_args *a) { if (!*a->arguments) return false; char *target = cut_word (&a->arguments); if (!*a->arguments) log_server_error (a->s, a->s->buffer, "No text to send"); else SEND_AUTOSPLIT_NOTICE (a->s, target, a->arguments); return true; } static bool handle_command_ctcp (struct handler_args *a) { if (!*a->arguments) return false; char *target = cut_word (&a->arguments); if (!*a->arguments) return false; char *tag = cut_word (&a->arguments); transform_str (tag, toupper_ascii); if (*a->arguments) irc_send (a->s, "PRIVMSG %s :\x01%s %s\x01", target, tag, a->arguments); else irc_send (a->s, "PRIVMSG %s :\x01%s\x01", target, tag); if (!a->s->cap_echo_message) log_ctcp_query (a->s, target, tag); return true; } static bool handle_command_me (struct handler_args *a) { if (a->buffer->type == BUFFER_CHANNEL) SEND_AUTOSPLIT_ACTION (a->s, a->buffer->channel->name, a->arguments); else if (a->buffer->type == BUFFER_PM) SEND_AUTOSPLIT_ACTION (a->s, a->buffer->user->nickname, a->arguments); else log_server_error (a->s, a->s->buffer, "Can't do this from a server buffer (#s)", "send CTCP actions"); return true; } static bool handle_command_quit (struct handler_args *a) { struct str_map_iter iter; str_map_iter_init (&iter, &a->ctx->servers); struct server *s; while ((s = str_map_iter_next (&iter))) { if (irc_is_connected (s)) irc_initiate_disconnect (s, *a->arguments ? a->arguments : NULL); } initiate_quit (a->ctx); return true; } static bool handle_command_join (struct handler_args *a) { // XXX: send the last known channel key? if (irc_is_channel (a->s, a->arguments)) // XXX: we may want to split the list of channels irc_send (a->s, "JOIN %s", a->arguments); else if (a->buffer->type != BUFFER_CHANNEL) log_server_error (a->s, a->buffer, "#s: #s", "Can't join", "no channel name given and this buffer is not a channel"); // TODO: have a better way of checking if we're on the channel else if (a->buffer->channel->users) log_server_error (a->s, a->buffer, "#s: #s", "Can't join", "you already are on the channel"); else if (*a->arguments) irc_send (a->s, "JOIN %s :%s", a->buffer->channel->name, a->arguments); else irc_send (a->s, "JOIN %s", a->buffer->channel->name); return true; } static void part_channel (struct server *s, const char *channel_name, const char *reason) { if (*reason) irc_send (s, "PART %s :%s", channel_name, reason); else irc_send (s, "PART %s", channel_name); } static bool handle_command_part (struct handler_args *a) { if (irc_is_channel (a->s, a->arguments)) { struct str_vector v; str_vector_init (&v); split_str_ignore_empty (cut_word (&a->arguments), ' ', &v); for (size_t i = 0; i < v.len; i++) part_channel (a->s, v.vector[i], a->arguments); str_vector_free (&v); } else if (a->buffer->type != BUFFER_CHANNEL) log_server_error (a->s, a->buffer, "#s: #s", "Can't part", "no channel name given and this buffer is not a channel"); // TODO: have a better way of checking if we're on the channel else if (!a->buffer->channel->users) log_server_error (a->s, a->buffer, "#s: #s", "Can't part", "you're not on the channel"); else part_channel (a->s, a->buffer->channel->name, a->arguments); return true; } static void cycle_channel (struct server *s, const char *channel_name, const char *reason) { // If a channel key is set, we must specify it when rejoining const char *key = NULL; struct channel *channel; if ((channel = str_map_find (&s->irc_channels, channel_name))) key = str_map_find (&channel->param_modes, "k"); if (*reason) irc_send (s, "PART %s :%s", channel_name, reason); else irc_send (s, "PART %s", channel_name); if (key) irc_send (s, "JOIN %s :%s", channel_name, key); else irc_send (s, "JOIN %s", channel_name); } static bool handle_command_cycle (struct handler_args *a) { if (irc_is_channel (a->s, a->arguments)) { struct str_vector v; str_vector_init (&v); split_str_ignore_empty (cut_word (&a->arguments), ' ', &v); for (size_t i = 0; i < v.len; i++) cycle_channel (a->s, v.vector[i], a->arguments); str_vector_free (&v); } else if (a->buffer->type != BUFFER_CHANNEL) log_server_error (a->s, a->buffer, "#s: #s", "Can't cycle", "no channel name given and this buffer is not a channel"); // TODO: have a better way of checking if we're on the channel else if (!a->buffer->channel->users) log_server_error (a->s, a->buffer, "#s: #s", "Can't cycle", "you're not on the channel"); else cycle_channel (a->s, a->buffer->channel->name, a->arguments); return true; } static bool handle_command_mode (struct handler_args *a) { // Channel names prefixed by "+" collide with mode strings, // so we just disallow specifying these channels char *target = NULL; if (strchr ("+-\0", *a->arguments)) { if (a->buffer->type == BUFFER_CHANNEL) target = a->buffer->channel->name; if (a->buffer->type == BUFFER_PM) target = a->buffer->user->nickname; if (a->buffer->type == BUFFER_SERVER) target = a->s->irc_user->nickname; } else // If there a->arguments and they don't begin with a mode string, // they're either a user name or a channel name target = cut_word (&a->arguments); if (!target) log_server_error (a->s, a->buffer, "#s: #s", "Can't change mode", "no target given and this buffer is neither a PM nor a channel"); else if (*a->arguments) // XXX: split channel mode params as necessary using irc_max_modes? irc_send (a->s, "MODE %s %s", target, a->arguments); else irc_send (a->s, "MODE %s", target); return true; } static bool handle_command_topic (struct handler_args *a) { if (*a->arguments) // FIXME: there's no way to unset the topic irc_send (a->s, "TOPIC %s :%s", a->channel_name, a->arguments); else irc_send (a->s, "TOPIC %s", a->channel_name); return true; } static bool handle_command_kick (struct handler_args *a) { if (!*a->arguments) return false; char *target = cut_word (&a->arguments); if (*a->arguments) irc_send (a->s, "KICK %s %s :%s", a->channel_name, target, a->arguments); else irc_send (a->s, "KICK %s %s", a->channel_name, target); return true; } static bool handle_command_kickban (struct handler_args *a) { if (!*a->arguments) return false; char *target = cut_word (&a->arguments); if (strpbrk (target, "!@*?")) return false; // XXX: how about other masks? irc_send (a->s, "MODE %s +b %s!*@*", a->channel_name, target); if (*a->arguments) irc_send (a->s, "KICK %s %s :%s", a->channel_name, target, a->arguments); else irc_send (a->s, "KICK %s %s", a->channel_name, target); return true; } static void mass_channel_mode (struct server *s, const char *channel_name, bool adding, char mode_char, struct str_vector *v) { size_t n; for (size_t i = 0; i < v->len; i += n) { struct str modes; str_init (&modes); struct str params; str_init (¶ms); n = MIN (v->len - i, s->irc_max_modes); str_append_printf (&modes, "MODE %s %c", channel_name, "-+"[adding]); for (size_t k = 0; k < n; k++) { str_append_c (&modes, mode_char); str_append_printf (¶ms, " %s", v->vector[i + k]); } irc_send (s, "%s%s", modes.str, params.str); str_free (&modes); str_free (¶ms); } } static void mass_channel_mode_mask_list (struct handler_args *a, bool adding, char mode_char) { struct str_vector v; str_vector_init (&v); split_str_ignore_empty (a->arguments, ' ', &v); // XXX: this may be a bit too trivial; we could map also nicknames // to information from WHO polling or userhost-in-names for (size_t i = 0; i < v.len; i++) { char *target = v.vector[i]; if (strpbrk (target, "!@*?")) continue; v.vector[i] = xstrdup_printf ("%s!*@*", target); free (target); } mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v); str_vector_free (&v); } static bool handle_command_ban (struct handler_args *a) { if (*a->arguments) mass_channel_mode_mask_list (a, true, 'b'); else irc_send (a->s, "MODE %s +b", a->channel_name); return true; } static bool handle_command_unban (struct handler_args *a) { if (*a->arguments) mass_channel_mode_mask_list (a, false, 'b'); else return false; return true; } static bool handle_command_invite (struct handler_args *a) { struct str_vector v; str_vector_init (&v); split_str_ignore_empty (a->arguments, ' ', &v); bool result = !!v.len; for (size_t i = 0; i < v.len; i++) irc_send (a->s, "INVITE %s %s", v.vector[i], a->channel_name); str_vector_free (&v); return result; } static struct server * resolve_server (struct app_context *ctx, struct handler_args *a, const char *command_name) { struct server *s = NULL; if (*a->arguments) { char *server_name = cut_word (&a->arguments); if (!(s = str_map_find (&ctx->servers, server_name))) log_global_error (ctx, "/#s: #s: #s", command_name, "no such server", server_name); } else if (a->buffer->type == BUFFER_GLOBAL) log_global_error (ctx, "/#s: #s", command_name, "no server name given and this buffer is global"); else s = a->buffer->server; return s; } static bool handle_command_connect (struct handler_args *a) { struct server *s = NULL; if (!(s = resolve_server (a->ctx, a, "connect"))) return true; if (irc_is_connected (s)) { log_server_error (s, s->buffer, "Already connected"); return true; } if (s->state == IRC_CONNECTING) irc_destroy_connector (s); irc_cancel_timers (s); irc_initiate_connect (s); return true; } static bool handle_command_disconnect (struct handler_args *a) { struct server *s = NULL; if (!(s = resolve_server (a->ctx, a, "disconnect"))) return true; if (s->state == IRC_CONNECTING) { log_server_status (s, s->buffer, "Connecting aborted"); irc_destroy_connector (s); } else if (!irc_is_connected (s)) log_server_error (s, s->buffer, "Not connected"); else irc_initiate_disconnect (s, *a->arguments ? a->arguments : NULL); return true; } static void show_servers_list (struct app_context *ctx) { log_global_indent (ctx, ""); log_global_indent (ctx, "Servers list:"); struct str_map_iter iter; str_map_iter_init (&iter, &ctx->servers); struct server *s; while ((s = str_map_iter_next (&iter))) log_global_indent (ctx, " #s", s->name); } static bool handle_server_add (struct handler_args *a) { char *name = cut_word (&a->arguments); if (!*a->arguments) return false; struct app_context *ctx = a->ctx; if (check_server_name_for_addition (ctx, name)) server_add_new (ctx, name); return true; } static bool handle_command_server (struct handler_args *a) { struct app_context *ctx = a->ctx; char *action = cut_word (&a->arguments); bool result = true; if (!strcasecmp_ascii (action, "list")) show_servers_list (ctx); else if (!strcasecmp_ascii (action, "add")) result = handle_server_add (a); else if (!strcasecmp_ascii (action, "remove")) ; // TODO: else if (!strcasecmp_ascii (action, "rename")) ; // TODO: else result = false; return result; } static bool handle_command_names (struct handler_args *a) { char *channel_name = try_get_channel (a, maybe_cut_word); if (channel_name) irc_send (a->s, "NAMES %s", channel_name); else irc_send (a->s, "NAMES"); return true; } static bool handle_command_whois (struct handler_args *a) { if (*a->arguments) irc_send (a->s, "WHOIS %s", a->arguments); else if (a->buffer->type == BUFFER_PM) irc_send (a->s, "WHOIS %s", a->buffer->user->nickname); else if (a->buffer->type == BUFFER_SERVER) irc_send (a->s, "WHOIS %s", a->s->irc_user->nickname); else log_server_error (a->s, a->buffer, "#s: #s", "Can't request info", "no target given and this buffer is not a PM nor a server"); return true; } static bool handle_command_whowas (struct handler_args *a) { if (*a->arguments) irc_send (a->s, "WHOWAS %s", a->arguments); else if (a->buffer->type == BUFFER_PM) irc_send (a->s, "WHOWAS %s", a->buffer->user->nickname); else log_server_error (a->s, a->buffer, "#s: #s", "Can't request info", "no target given and this buffer is not a PM"); return true; } static bool handle_command_nick (struct handler_args *a) { if (!*a->arguments) return false; irc_send (a->s, "NICK %s", cut_word (&a->arguments)); return true; } static bool handle_command_quote (struct handler_args *a) { irc_send (a->s, "%s", a->arguments); return true; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static bool handle_command_channel_mode (struct handler_args *a, bool adding, char mode_char) { if (!*a->arguments) return false; struct str_vector v; str_vector_init (&v); split_str_ignore_empty (a->arguments, ' ', &v); mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v); str_vector_free (&v); return true; } #define CHANMODE_HANDLER(name, adding, mode_char) \ static bool \ handle_command_ ## name (struct handler_args *a) \ { \ return handle_command_channel_mode (a, (adding), (mode_char)); \ } CHANMODE_HANDLER (op, true, 'o') CHANMODE_HANDLER (deop, false, 'o') CHANMODE_HANDLER (voice, true, 'v') CHANMODE_HANDLER (devoice, false, 'v') #define TRIVIAL_HANDLER(name, command) \ static bool \ handle_command_ ## name (struct handler_args *a) \ { \ if (*a->arguments) \ irc_send (a->s, command " %s", a->arguments); \ else \ irc_send (a->s, command); \ return true; \ } TRIVIAL_HANDLER (list, "LIST") TRIVIAL_HANDLER (who, "WHO") TRIVIAL_HANDLER (motd, "MOTD") TRIVIAL_HANDLER (stats, "STATS") TRIVIAL_HANDLER (away, "AWAY") // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static bool handle_command_help (struct handler_args *); static struct command_handler { const char *name; const char *description; const char *usage; bool (*handler) (struct handler_args *a); enum handler_flags flags; } g_command_handlers[] = { { "help", "Show help", "[ |